Table of Contents
- The ZIO Scaladoc
- ZIO + Scala CLI + Scala 3
- ZIO build.sbt configuration
- How to create a ZIO 2 application
- Implementing the ‘run’ method
- ZIO Console I/O
- Sequential composition/computation
- Sequential operators
- ZIO type parameters
- ZIO type aliases
- delay, run later, timer/clock
- Error handling
- ZIO 2 retries and scheduling
- A Tour of ZIO (video)
- Using ZIO in the Ammonite REPL
- Resources
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 anEither[E, A]
- to either an
E
or anA
- function takes an
R
E
is a failure type,A
is a success type- you give the functions an
R
and get anE
or anA
- always get a failure or a success
- to either an
- 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
- a function of the
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.