ZIO 2: From flatMap to for-Expressions (Scala 3 Video)
ZIO 104: ZIO.attempt, ZIO.succeed
NOTE: This text comes from my book, Learn Functional Programming The Fast Way. (The PDF version of that book is FREE at this Gumroad.com URL.) Also, the flatMap
discussion I mention is a few paragraphs down from here.
If we assume that we have the user’s name, we now need a way to functionally print that name. Again we imagine that the ZIO Console
type doesn’t exist, so we need to handle this ourselves. For this I’ll go back to the way of writing functions that I used in most of this book: a step-by-step function-sketching process.
First, we know that we eventually want to use the Scala println
function to print some text. You may not have any way of knowing this yet, but because I’ve done this before, I know that I want to create a ZIO-based printing function that starts like this:
def printName(name: String): ZIO[???] =
// some use of println here
This is a function that takes a String
input parameter, returns some sort of ZIO
type, and uses println
in its body.
Next, because I already mentioned ZIO.succeed
, I’ll jump inside the function’s body to show how to wrap println
with it:
def printName(name: String): ZIO[???] =
ZIO.succeed(println(s"Hello, $name"))
As mentioned, whenever a block of code can’t fail, just wrap it in ZIO.succeed
.
To me, the only way println
can fail is if something is really wrong with a computer, so it meets this definition. More importantly, its function signature doesn’t state that it can throw exceptions.
Now all we have to do is fill in the ZIO
type the function returns. To do that, we know these things:
- The function doesn’t require any sort of special environment, like a database, so its type is
Any
. - We’re saying that the function body cannot fail —
println
throws no exceptions — so its error type parameter isNothing
. With ZIO, any time a function body can’t fail, you use this Scala type. - Because
println
prints to STDOUT, it has theUnit
return type, and because that’s the final value of the function, it becomes the function’s “success” value, and is used as the thirdZIO
parameter.
Given that thought process, here’s the complete function:
def printName(name: String): ZIO[Any, Nothing, Unit] =
ZIO.succeed(println(s"Hello, $name"))
So now we have ways to prompt a user for input, read their input, and print their input. Now we just need a way to call those functions in sequence.
Hello, flatMap
Excluding the necessary import
statements, I’ll jump to the complete solution for this problem:
object Zio104 extends ZIOAppDefault:
val username: ZIO[Any, Throwable, String] =
ZIO.attempt(StdIn.readLine("What’s your name? "))
def printName(name: String): ZIO[Any, Nothing, Unit] =
ZIO.succeed(println(s"Hello, $name"))
val run = username.flatMap { name =>
printName(name)
}
The new code in this solution is this:
val run = username.flatMap { name =>
printName(name)
}
Here are the keys to this code:
run
is the usualZIOAppDefault
code. It says, “Run the code on the right side of the=
symbol as my ZIO application.”flatMap
is the new, interesting piece. In ZIO,flatMap
is THE mechanism that is used to run functional effects in sequence.- A key to that last statement is that
ZIO
s are described as “blueprints for running concurrent workflows.” You can take that to mean that anyZIO
effect — such asusername
andprintName
— can potentially be run concurrently. - Therefore,
flatMap
is a way to ensure that in a concurrent environment, one effect is run first, and then its output is given to the next effect, which is run second. We say that the two effects are run in sequence, and this is sequential composition. - In this example, the use of
flatMap
means, “Runusername
, and when it finishes, pass its result as the variable I have namedname
to theprintName
function, and then run that function.” - A cool thing is that this statement is true whether the functions are implemented to run in serial or in parallel. You can almost think of that as a runtime detail that you don’t have to concern yourself with; you just use
flatMap
.
Now when you run Zio104
with Scala CLI and your name happens to be Alvin
, the interaction looks like this:
$ scala-cli Zio104.scala
What’s your name? Alvin
Hello, Alvin
Congratulations, again — you’re now sequentially composing effects!
ZIO 105: From flatMap to for-expressions
When you only have one or two effects that need to be run in sequence, you can use flatMap
as shown. However, when you need to run more effects in sequence, that sort of code can get hard to read:
effect1.flatMap { var1 =>
// use var1 in here somewhere
effect2.flatMap { var2 =>
// use var2 and/or var1 in here somewhere
effect3.flatMap { var3 =>
// use var1, var2, and var3 as needed
}
}
}
Frankly, nobody wants to read code like that. Fortunately this is where the Scala for
expression comes in: the for
expression is quite literally syntactic sugar for that code. This means that this for
expression is the equivalent of that flatMap
code:
for
var1 <- effect1
var2 <- effect2(var1)
var3 <- effect3(var2)
yield
()
More accurately, if I had taken the time to write this for
expression correctly — and if you dug into the Scala compilation process — you’d see that the Scala compiler turns this for
expression into a series of flatMap
method calls (followed by a single map
method call).
Frankly, my translation isn’t 100% correct because it’s not that important. The important thing is that in the Zio105
application, you can change this flatMap
code:
val run = username.flatMap { name =>
printName(name)
}
to this for
expression:
val run =
for
name <- username
_ <- printName(name)
yield
()
and I do know that this code is 100% equivalent, because I did test it. And again, when you add more functional effects in sequence, writing them inside for
expressions makes them much easier to read (compared to writing a series of flatMap
s).
Most importantly, this is what ZIO sequential composition looks like: use
flatMap
to sequence one or two effects (if you want to), and usefor
expressions to sequence many effects.
So now, when I replace the previous flatMap
code with this new for
expression code, the next complete version of our application (without import
statements) looks like this:
object Zio105 extends ZIOAppDefault:
val username: ZIO[Any, Throwable, String] =
ZIO.attempt(StdIn.readLine("What’s your name? "))
def printName(name: String): ZIO[Any, Nothing, Unit] =
ZIO.succeed(println(s"Hello, $name"))
// new: replace `flatMap` with a `for` expression
val run =
for
name <- username
_ <- printName(name)
yield
()
When you run this new Zio105
application, you’ll see that it works just like Zio104
works:
$ scala-cli Zio105.scala
What’s your name? Alvin
Hello, Alvin
For many more details on how
for
expressions translate to a series offlatMap
andmap
methods, see my book, Functional Programming, Simplified.
ZIO 106: ZIO aliases
And now it’s time for the last thing I need to demonstrate before I turn you over to the ZIO (and Cats Effect) documentation: ZIO makes common use of Scala type aliases (and those common ZIO aliases are documented here).
JIT: Scala type aliases
A Scala type alias is just a way to give a new name to another type, and hopefully the alias is shorter and/or more meaningful than the original type. A very basic use of a type alias named Money
looks like this in the Scala REPL:
scala> type Money = Double
// defined alias type Money = Double
scala> val oneDollar: Money = 1.00
val oneDollar: Money = 1.0
When you’re working with a financial application, it can be more meaningful to use the type name Money
rather than Double
, and Scala lets you do this. (Please bear in mind that this is a very basic example that doesn’t show everything you need for a real-world application.)
Type aliases in ZIO
Per the ZIO type alias documentation, the ZIO type alias UIO[A]
is a type alias for ZIO[Any, Nothing, A]
.
You use this alias any time (a) the environment is not important, (b) the error type is not important, and the only type that is important is the “success type,” A
. Therefore, ZIO refers to this as an Unexceptional effect, so you can replace this ZIO
return type that I used in Zio105
:
def printName(name: String): ZIO[Any, Nothing, Unit] =
-----------------------
with the alias, UIO
:
def printName(name: String): UIO[Unit] =
---------
Once you get used to this approach, I believe you’ll find that this is a terrific use of Scala type aliases.
So now, if you replace that ZIO
return type in printName
’s signature in the Zio105
example with the UIO
return type, and re-run the application, you’ll see the exact same result as before. (This is Zio106
in the book’s Github repository.)
And now, before I leave you, here are the common ZIO 2 type aliases:
ALIAS ORIGINAL/FULL TYPE
---------- ----------------------
IO[E, A] ZIO[Any, E, A]
Task[A] ZIO[Any, Throwable, A]
RIO [R, A] ZIO[R, Throwable, A]
UIO [A] ZIO[Any, Nothing, A]
URIO[R, A] ZIO[R, Nothing, A]
I like to think of types portions of code as running a task, so the Task
alias is an interesting one to look at.
As shown, Task[A]
is an alias for ZIO[Any, Throwable, A]
, which means:
- A
Task
has no requirements for its environment (Any
), - it can throw an exception (
Throwable
), and - if it succeeds, it succeeds with a result that has the type
A
.
For more information on using these type aliases, see the ZIO type alias documentation.
What’s next?
I could happily write about ZIO and Cats Effect for an entire book, but alas, it’s time for me to turn you over to their websites!
As I stated in the beginning of this book, this book’s reason for existence is to get you to the point where you can look at the Cats Effect and ZIO documentation and think, “I know what they’re doing, and why they’re doing it.” I think — or at least hope — that I’ve accomplished this task, and when you look at their documentation, I hope you’ll agree.
Update: All of my new videos are now on
LearnScala.dev