home search about rss feed twitter ko-fi

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 is Nothing. With ZIO, any time a function body can’t fail, you use this Scala type.
  • Because println prints to STDOUT, it has the Unit return type, and because that’s the final value of the function, it becomes the function’s “success” value, and is used as the third ZIO 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 usual ZIOAppDefault 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 ZIOs are described as “blueprints for running concurrent workflows.” You can take that to mean that any ZIO effect — such as username and printName — 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, “Run username, and when it finishes, pass its result as the variable I have named name to the printName 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 flatMaps).

Most importantly, this is what ZIO sequential composition looks like: use flatMap to sequence one or two effects (if you want to), and use for 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 of flatMap and map 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