22 Intro To Zio
22 Intro To Zio
2023
Index
• Effects as blueprints
• Sequential operators
• ZIO type parameters
• ZIO type aliases
• Comparison to Future
• More effect constructors
• Default ZIO services
• Recursion and ZIO
Thinking in ZIO
• ZIO helps you build applications that are
– concurrent
– resilient
– efficient
• But to use ZIO requires thinking about software in a different way
– from the functional programming perspective
1
Functional Effects as Blueprints
• In traditional procedural programming, our programs interacts directly
with the outside world
val goShoppingUnsafe: Unit =
println("Going to the grocery store")
when Scala computes the value to assign, it directly interacts with the
outside world printing a message in the console
• Procedural programming
– it’s convenient for simple programs
– but entangles what we want to do with how
scheduler.schedule(
new Runnable { def run: Unit = goShoppingUnsafe },
1,
HOURS
)
scheduler.shutdown()
• But this code has a bug
– we only schedule the returning of the Unit value ()
– the println is executed before
– Solution: use a def
2
val goShopping =
ZIO.attempt(println("Going to the grocery store"))
val goShoppingLater =
goShopping.delay(1.HOUR)
Sequential Composition
• ZIO programs are build by transforming and combining smaller, simpler
effects
• E.g. we have seen the delay method, which transforms one effect into
another whose execution will be delayed into the future
• One of the most important operators is flatMap
trait ZIO[R, E, A]:
def flatMap[B](andThen: A => ZIO[R, E, B]): ZIO[R, E, B]
• The result of flatMap is a workflow (that is, a description) that, when
executed
– first will run the first effect
– and then run a second one that depends on the result of the first one
Sequential Composition
import scala.io.StdIn
3
• Notice how what we print depends on what we’ve read
Sequential composition
• The same program can be written using a for comprehension
import scala.io.StdIn
val echo =
for
line <- readLine
_ <- printLine(line)
yield ()
val lastName =
ZIO.attempt(StdIn.readLine("What is your last name?"))
val fullName =
firstName.zipWith(lastName)((first, last) => s"$first $last")
• This operator is less powerful that flatMap because the second effect
cannot depend on the result of the first one.
– Even thought the execution is sequential
• NOTE: Do you remember map2?
4
val helloWorld =
ZIO.attempt(print("Hello, ")) *> ZIO.attempt(println("World!"))
val printWords =
ZIO.collectAll(prints)
• There is also a collectAllDiscard which discards the results (returns a
ZIO[R, E, Unit]).
• NOTE: Do you remember sequence?
5
– E is the type of value the effect can fail with.
∗ this could be Throwable or Exception, or a domain-specific error
type.
∗ if the effect does not fail at all, the parameter is Nothing
– A is the type of value the effect can succeed with
∗ it can be though as the return value (or the output) of the effect
• A toy model for this effect type is
final case class ZIO[-E, +E, +A](run: R => Either[E, A])
def flatMap[R1 <: R, E1 >: E, B](f: A => ZIO[R1, E1, B]): ZIO[R1, E1, B] =
ZIO(r => self.run(r).fold(ZIO.fail(_), f).run(r))
object ZIO:
6
lazy val readAndSumTwoInts: ZIO[Any, NumberFormatException, Int] =
for
x <- readInt
y <- readInt
yield x + y
• The error type shows how this functions can fail given its signature
• We can operate on the results of the effects assuming they are successful,
deferring errors to later
7
ZIO Type parameters
The Environment Type
• The two fundamental operations are accessing the environment (e.g. getting
the database to do something with it) and providing the environment
(e.g. providing the database service).
final case class ZIO[-R, +E, +A](run: R => Either[E, A]):
self =>
object ZIO:
def environment[R]: ZIO[R, Nothing, R] =
ZIO(r => Right(r))
Comparison to Future
• Unlike ZIO, a Future is a running effect
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
8
Comparison to Future
• With Future we have a persistent requirement for ExecutionContext in
scope whenever you call methods on future
import scala.concurrent.ExecutionContext
trait Future[+A]:
def flatMap[B](f: A => Future[B])(using ec: ExecutionContext): Future[B]
• Future#flatMap requires an ExecutionContext because it represents a
running computation, so we need one on which this code should immediately
work
Comparison to Future
• Future fixes the error to Throwable:
import scala.util.Try
trait Future[+A]:
def onComplete[B](f: Try[A] => B): Unit
• The result of a Future can be a Success with an A or a Failure with a
Throwable
Comparison to Future
• This impedes reasoning about the errors or about its absence !!!
def parseInt: Future[Int] = ???
Comparison to Future
• Finally, Future do not have a way to model its dependencies
• This requires either:
– manual injection
– third party libraries
9
More Effect Constructors
• Before, we have presented the ZIO.attempt constructor to convert proce-
dural code into ZIO
• It takes side-effecting code and converts it into a pure value which merely
describes side-effects.
• But it is not suitable in every scenario:
– It returns always a ZIO[Any, Throwable, A]
– Requires the procedural code to be synchronous
– It assume that the value is not wrapped in another type that handles
failure (such as Option, Either, . . . )
object ZIO:
def fromOption[A](oa: => Option[A]): IO[None.type, A] = ???
def fromEither[E, A](eea: => Either[E, A]): IO[E, A] = ???
def fromTry[A](ta: => Try[A]): Task[A] = ???
10
object ZIO:
def attempt[A](a: => A): ZIO[Any, Throwable, A] = ???
def succeed[A](a: => A): ZIO[Any, Nothing, A] = ???
getUserByIdAsync(0) {
case Some(name) => println(name)
case None => println("User not found")
}
• Callback based APIs can improve performance, but working directly with
them can be quite painful
– highly nested code (callback hell)
11
More Effect Constructors
From Futures
• Some async APIs are expresses by Futures, and so we have a
ZIO.fromFuture constructor as well
object ZIO:
def fromFuture[A](make: ExecutionContext => Future[A]): Task[A] = ???
• Although you don’t need to use the provided ExecutionContext when you
convert a Futureto ZIO, if you use it, ZIO can manage where the Future
runs
def goShoppingFuture(using ec: ExecutionContext): Future[Unit] =
Future(println("Going to the grocery store"))
12
• With sleep we can implement delay:
def delay[R, E, A](zio: ZIO[R, E, A])(duration: Duration) =
Clock.sleep(duration) *> zio
13
val readInt: ZIO[Any, Throwable, Int] =
for
line <- Console.readLine
n <- ZIO.attempt(line.toInt)
yield n
• We can build an effect that retries until getting an integer this way
val readIntOrRetry: ZIO[Any, Nothing, Int] =
readInt
.orElse(Console.printLine("Please enter a valid integer").orDie *> readIntOrRetry)
Bibliography
• ZIO Homepage
• ZIO API
• J. De Goes and A. Fraser. Zionomicon. Gumroad, TBP. https://www.
zionomicon.com/
• D. Ciocîrlan. “ZIO 2.0 Course”. https://rockthejvm.com (Access: 2022-10-
10)
• Ziverge, “Zymposium- ZIO from scratch (ZIO 2 runtime)”:
1. Hackaton Edition
2. ZIO From Scratch
3. ZIO From Scratch (Part 2)
4. ZIO From Scratch (Part 3)
5. ZIO From Scratch (Part 4)
6. ZIO From Scratch (Final)
14