A ZIO 2 cheatsheet

April, 2024 Update: This ZIO cheatsheet is currently being updated to ZIO 2.x, but it’s still a work in progress.

If you want a good cheat sheet right now, see this one on github. I’m creating my own as I learn ZIO and read the ZIOnomicon book. During the learning process I find that it’s much better to create your own by hand, that way you get something that’s meaningful to you.

Note that almost all of these initial examples come from the ZIOnomicon book and the video that I link to later.

The ZIO Scaladoc

Here’s a link to the ZIO Scaladoc. That’s for the companion object, and this link is for the companion trait.

ZIO + Scala CLI + Scala 3

November, 2022 update: Here are two ZIO + Scala CLI + Scala 3 “Hello, world” examples.

ZIO build.sbt configuration

Use the latest stable version:

libraryDependencies += "dev.zio" %% "zio" % "2.0.22"

See the ZIO Getting Started page for more info. It also shows zio.App and zio.console. examples.

How to create a ZIO 2 application

To create a ZIO 2 application, create a Scala object that extends ZIOAppDefault, and implement its run method:

import zio.*

object ZioHttp extends ZIOAppDefault:
    def run = Console.printLine("Hello, world")

Implementing the ‘run’ method

There are quite a few ways to implement the run method. Assuming that your main application is in a variable named blueprint, here are some possible ways to implement run:

foldZIO

val run =
    blueprint.foldZIO(
        failure => Console.printLineError(s"FAILURE = $failure"),
        success => Console.printLine(     s"SUCCESS = $success")
    )

fold

val run = blueprint.fold(
    error   => println(s"ERROR:   $error"),
    success => println(s"SUCCESS: $success")
)

flatMap

val run = blueprint.flatMap { (lines: Seq[String]) =>
    ZIO.foreach(lines) { line =>
        Console.printLine(line)
    }
}

for-expression

This is a complete ZIO 2 application defined in a run method:

val run: ZIO[NoEnv, Throwable, Unit] =
    for
        rez <- zMakeInt("hi")
        _   <- Console.printLine(s"STDOUT: ${rez}")
    yield
        ()

exit and exitCode

def run = 
    for
        exitCode <- blueprint.exit
        _        <- exitCode match
                        case Exit.Success(value) =>
                            printLine(s"\nEXIT: SUCCESS: ${value}")
                        case Exit.Failure(cause) =>
                            printLine(s"\nEXIT: FAILURE: $cause")
    yield
          ()

provide + foldZIO

val run =
    blueprint.provide(
        Client.default,
        Scope.default
    ).foldZIO(
        failure => Console.printLineError(s"failure = $failure"),
        success => Console.printLine(s"success = $success")
    )

provide + flatMap

val run =
    blueprint.provide(
        Client.default,
        Scope.default
    ).flatMap(todos => ZIO.foreach(todos) {
        todo => Console.printLine(todo)
})

// OR

override val run =
    blueprint.provide(
        Client.default,
        Scope.default
    ).flatMap(todo => Console.printLine(todo))

To demonstrate one of those approaches, here’s a small but complete ZIO 2 application written with Scala 3:

import zio.*
import zio.Console.*

def zMakeInt(s: String): ZIO[Any, NumberFormatException, Int] =

     ZIO.attempt(s.toInt)
        .refineToOrDie[NumberFormatException]

object ZioExample extends ZIOAppDefault:

    val blueprint: ZIO[Any, NumberFormatException, Int] =
        for
            a <- zMakeInt("1")
            b <- zMakeInt("uh oh")
            c <- zMakeInt("3")
        yield
            a + b + c

    val run = blueprint.foldZIO(
        failure => printLineError(s"FAILURE = $failure"),
        success => printLine(     s"SUCCESS = $success")
    )

In that example, my main application code is in the blueprint variable, and then that variable is run and handled inside the run method.

I call the variable blueprint because writing a ZIO application is like creating a blueprint or writing a long equation, and then you run the equation and handle its results in the run method, as shown here.

(IMHO, when you’re first starting with ZIO 2, other good names for your code are equation, myEquation, myAlgebra, or something more boring and less math-like, such as program.)

UPDATE: See my article on Examples of how to implement the ZIO 2 'run' method for more solutions.

ZIO Console I/O

When many of us start with a new technology, we start with “Hello, world” examples of reading input and printing output. In ZIO 2, you use the Console type for those actions. Console has these functions for console I/O:

  • print            // like 'print'
  • printError       // like 'System.err.print'
  • printLine         // like 'println'
  • printLineError   // like 'System.err.println'
  • readLine         // read input

See the ZIO Console page for more details.

Sequential composition/computation

Methods like flatMap and zip* let you get back to sequential computation, just like procedural programming, i.e., doing one thing after another:

import scala.io.StdIn
val readLine = ZIO.effect(StdIn.readLine())
def printLine(line: String) = ZIO.effect(println(line))

// execute readLine, then pass its result to printLine.
// flatMap can be read like “and then do Expression2 with
// the result of Expression1”:
val echo = readLine.flatMap(line => printLine(line))
  • you can chain a bunch of flatMap’s together like this
  • but it’s easier to read a for-expression/comprehension
  • that last line of code is equivalent to this:
import zio._
val echo = for {
    line <- readLine
    _    <- printLine(line)
} yield ()

Sequential operators

These descriptions mostly come from the book, Zionomicon:

Operator Description
flatMap Sequential composition of two effects. Creates a 2nd effect based on the output of the 1st effect.
zip Sequentially combine the results of two effects into a tuple.
zipLeft Sequentially combine two effects, returning the result of the 1st.
zipRight Sequentially combine two effects, returning the result of the 2nd.
<* An alias for zipLeft
*> An alias for zipRight
ZIO.foreach Returns a single effect that describes performing an effect for each element of a collection. Similar to a for loop.
ZIO.collectAll Returns a single effect that collects the results of a collection of effects.
  • zipWith syntax:
val firstName = ZIO.effect(StdIn.readLine("..."))
val lastName = ZIO.effect(StdIn.readLine("..."))
val fullName = firstName.zipWith(lastName)((first, last) => s"$first $last")

How it works:

val fullName = firstName.zipWith(lastName)((first, last) => s"$first $last")
                  ^                 ^                   ^
               effect #1        effect #2     merge them w/ this function

Here’s an example that shows ZIO’s flatMap and foreach:

override val run = program.flatMap { (lines: Seq[String]) =>
    ZIO.foreach(lines) { line =>
        Console.printLine(line)
    }
}

Here’s another flatMap example:

// can also use a for-expression
val failWithMsgEffect =
    printLineError("Usage: yada yada...").flatMap { _ =>
        ZIO.fail(new Exception("Usage error"))
    }

For my own benefit, here’s the complete ZIO 2 application that code comes from:

object ZioReadFile extends ZIOAppDefault:

    val filename = "stocks.dat"

    val program = for
        linesFromFile: Seq[String] <- ZIO.fromTry(readFile(filename))
        cleanLines: Seq[String] = getRidOfUndesiredLines(linesFromFile)
    yield
        cleanLines

    override val run = program.flatMap { (lines: Seq[String]) =>
        ZIO.foreach(lines) { line =>
            Console.printLine(line)
        }
    }

There may be better ways to do that, but that’s what I’m doing today. See the ZIO Control Flow page for more details and examples of ZIO’s conditional and loop operators.

ZIO type parameters

Type Description
R The environment
E The possible error result type
A The possible (hopeful) success result type

See the ZIO Overview page for more details.

ZIO type aliases

Type aliases you can use instead of the full ZIO type signature:

Alias Full Type Signature
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]

See the ZIO Overview page for more details.

delay, run later, timer/clock

// ZIOnomicon example
import zio.clock._
import zio.duration._

val goShoppingLater = goShopping.delay(1.hour)

Error handling

TODO: These are some miscellaneous examples at the moment:

def parseInput(input: String): ZIO[Any, NumberFormatException, Int] =
  ZIO.attempt(input.toInt)                 // ZIO[Any, Throwable, Int]
    .refineToOrDie[NumberFormatException]  // ZIO[Any, NumberFormatException, Int]
import scala.util.control.Exception.*
val eitherEffect: ZIO[Any, Throwable, Int] = 
    ZIO.fromEither( allCatch.either("42".toInt) ) 
def zMakeInt(s: String): ZIO[Any, Nothing, Int] =
    ZIO.fromEither(makeInt(s))
       .catchAll(_ => ZIO.succeed(0))

ZIO 2 retries and scheduling

A Tour of ZIO (video)

These are my notes from watching the A Tour of ZIO video. They’re not very organized, but hopefully I’ll fix them up one day.

ZIO is based on fiber-based concurrency:

  • fibers are coming to the JVM via Project Loom
  • until then, you can use libraries like ZIO to get fibers now
  • ZIO is 100% async
    • even code that looks like it’s blocking isn’t blocking
  • uses type system to catch bugs at compile time when they’re easiest and cheapest to fix

Most important data type is called ZIO:

  • ZIO[-R, +E, +A]
    • can be thought of as a functional effect, or just effect
    • can be thought of as an immutable value
    • represents a job or workflow or task
    • can think of an effect as being a lazy description of various types of interactions with the outside world
      • printing text, databases, networks, etc.
    • ZIO effects are 100% lazy
    • nothing has happened; have to run “execution” to translate your description of the interaction with the outside world into actual interaction; that’s called running or executing the effect
    • do that with one of the “unsafe” method
    • R represents an environment you’re passing in

Mental model:

  • ZIO[R,E,A]
    • a function of the R parameter to an Either[E, A]
      • to either an E or an A
      • function takes an R
      • E is a failure type, A is a success type
      • you give the functions an R and get an E or an A
        • always get a failure or a success
    • an effect can be thought of as requiring some environment type R, which can be a database connection or some configuration or an http connection or spark client

ZIO has five type aliases that are common simplifications of ZIO[R,E,A]:

  • Five type aliases:
    • Task[+A] = ZIO[Any, Throwable, A]
      • an effect that doesn’t need anything
      • Any lets you model an effect that doesn’t need anything
    • UIO[+A] = ZIO[Any, Nothing, A]
      • doesn’t need anything
      • cannot fail (Nothing)
      • Nothing means the effect can’t fail
      • Nothing is a special type, there are no values of it
    • RIO[-R, +A] = ZIO[R, Throwable, A]
    • IO[+E, +A] = ZIO[Any, E, A]
      • does not need anything
      • fails with E or succeeds with A
    • URIO[-R,+A] = ZIO[R, Nothing, A]

zio.App:

  • zio.App
  • App expects you to override run
  • it also bakes in a runtime that executes the effect that is returned from run
  • run:
    • def run(args: List[String]): ZIO[ZEnv, Nothing, Int] = ...

HelloWorld:

  • have to return an effect from run
  • putStrLn prints a line of text and returns an effect
// an interactive application
object PromptName extends App {
    import zio.console._
    def run(args: List[String]): ZIO[ZEnv, Nothing, Int] = 
        putStrLn("What is your name? ") *>
        // can’t use zipRight operator here; if you want to do several
        // things in sequence, and what you’re working on depends on
        // the return/success values of what came before, you need to 
        // use flatMap, which is sometimes known as `chain` or `andThen`
        // in other languages
        getStrLn.flatMap(name => putStrLn(s"Hello, $name")).fold(
            _ => 1,
            _ => 0
        )
        
        // more readable:
        (for {
            // use _ because you don’t care about the Unit value
            _    <- putStrLn("What is your name? ")  // flatMap
            name <- getStrLn                         // flatMap
            _    <- putStrLn(s"Hello, $name")        // map
        } yield 0) orElse ZIO.succeed(1)
}

Using ZIO in the Ammonite REPL

If you want to use ZIO in the Ammonite REPL, here are some example import commands for that:

import $ivy.`dev.zio::zio:2.0.22`
import $ivy.`dev.zio::zio-http::3.0.0-RC4`
import $ivy.`dev.zio::zio-json::0.6.2`

Also, here’s a brief note about using ZIO 2 in the Ammonite REPL.

Resources