0% found this document useful (0 votes)
184 views14 pages

22 Intro To Zio

The document introduces ZIO, a library for building concurrent, resilient, and efficient applications in Scala. ZIO represents programs as effects (ZIO values) that are purely descriptive until executed. The core ZIO type, ZIO[R, E, A], represents an effect that requires an environment R, may fail with error E, and produces result A. Effects are combined using operators like flatMap and for comprehensions to build complex workflows in a functional style. ZIO helps avoid side effects by treating programs as asynchronous computations or "blueprints" instead of directly interacting with the outside world.

Uploaded by

Gamba Mercenaria
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
184 views14 pages

22 Intro To Zio

The document introduces ZIO, a library for building concurrent, resilient, and efficient applications in Scala. ZIO represents programs as effects (ZIO values) that are purely descriptive until executed. The core ZIO type, ZIO[R, E, A], represents an effect that requires an environment R, may fail with error E, and produces result A. Effects are combined using operators like flatMap and for comprehensions to build complex workflows in a functional style. ZIO helps avoid side effects by treating programs as asynchronous computations or "blueprints" instead of directly interacting with the outside world.

Uploaded by

Gamba Mercenaria
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 14

Introduction to ZIO

[email protected]

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

Functional Effects as Blueprints


• The core datatype is the ZIO[R, E, A] effect type
– is represents a blueprint for a concurrent workflow
– it’s purely descriptive in nature
– must be executed to observe any side-effects
• A workflow of type ZIO[R, E, A]
– requires you to supply a value (called environment) of type R when
you want to execute it
– it may fail with a value of type E
– or succeed with a value of type A

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

Functional Effects as Blueprints


• This entangling makes code difficult to understand, test and modify and
prone to subtle bugs
• Suppose we want to change when we want to go to the grocery store.
import java.util.concurrent.{ Executors, ScheduledExecutorService }
import java.util.concurrent.TimeUnit.*

val scheduler: ScheduledExecutorService =


Executors.newScheduledThreadPool(1)

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

Functional Effects as Blueprints


• This code using ZIO would be:
import zio.*

object GroceryStore extends ZIOAppDefault:

2
val goShopping =
ZIO.attempt(println("Going to the grocery store"))

val goShoppingLater =
goShopping.delay(1.HOUR)

val run = goShoppingLater

Functional Effects as Blueprints


• In ZIO, every ZIO effect is just a description, a blueprint for a concurrent
workflow
• We build our program by combining blueprints into much complex ones
• When we’re done, the final effect describes everything we need to do, and
we pass it to the runtime which executes the blueprint and produces the
result of the program (producing all the side effects)

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

val readLine = ZIO.attempt(StdIn.readLine())

def printLine(line: String) = ZIO.attempt(println(line))

val echo = readLine.flatMap(line => printLine(line))

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 readLine = ZIO.attempt(StdIn.readLine())

def printLine(line: String) = ZIO.attempt(println(line))

val echo =
for
line <- readLine
_ <- printLine(line)
yield ()

Other Sequential Operators


• ZIO provides numerous operators for sequential composition, for example
zipWith:
val firstName =
ZIO.attempt(StdIn.readLine("What is your first name?"))

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?

Other Sequential Operators


• Other variations include zip, which returns a tuple with the results;
zipLeft, which returns the first one; and zipRight, which returns the
second one
• These operators are usually written as <* and *>, respectively.

4
val helloWorld =
ZIO.attempt(print("Hello, ")) *> ZIO.attempt(println("World!"))

Other Sequential Operators


• Another useful operator is foreach which returns a single effect that
describes the performing of an effect for each element of a collection in
sequence
• It’s the equivalent of a foreach loop in procedural programming
val printNumbers =
ZIO.foreach(1 to 100) { n =>
printLine(n.toString)
}
• NOTE: Do you remember traverse?

Other Sequential Operators


• A similar operator is collectAll which returns a single effect that collects
the results of a collection of effects
val prints =
List(
printLine("The"),
printLine("quick"),
printLine("brown"),
printLine("fox")
)

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?

ZIO Type parameters


• A value of type ZIO[R, E, A] is a blueprint for a concurrent workflow
– R is the environment required for the effect to be executed
∗ this could include any dependencies the effect has (e.g. a connec-
tion to database)
∗ if it does not require anything the parameter will be Any

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])

ZIO Type parameters


• Let’s use this toy model to implement some parameters to better understand
it
final case class ZIO[-R, +E, +A](run: R => Either[E, A]):
self =>

def map[B](f: A => B): ZIO[R, E, B] =


ZIO(r => self.run(r).map(f))

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:

def attempt[A](a: => A): ZIO[Any, Throwable, A] =


ZIO(_ =>
try Right(a)
catch { case t if NonFatal(t) => Left(t) }
)

def fail[E](e: => E): ZIO[Any, E, Nothing] =


ZIO(_ => Left(e))

ZIO Type parameters


The Error Type
• The error type represents the potential ways an effect can fail.
• It’s helpful because it allows us to use operators that work on the success
type while deferring error handling until higher levels
lazy val readInt: ZIO[Any, NumberFormatException, Int] = ???

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

ZIO Type parameters


The Error Type
• To handle errors, let’s implement an operator named foldZIO, which let
us perform one effect if the original one fails and another if it succeeds
final case class ZIO[-R, +E, +A](run: R => Either[E, A]) =
self =>

def foldZIO[R1 <: R, E1, B](


failure: E => ZIO[R1, E1, B],
success: A => ZIO[R1, E1, B]
): ZIO[R1, E1, B] =
ZIO(r => self.run(r).fold(failure, success).run(r))

ZIO Type parameters


The Error Type
• One of the most useful features of the error type is that we can express
the fact that the effect cannot fail at all (maybe because all errors have
been handled)
– We say this by using the type Nothing, which has no values
– So a value of Either[Nothing, A] can only be a Right[A]
final case class ZIO[-R, +E, +A](run: R => Either[E, A]) =
self =>

def fold[B](failure: E => B, success: A => B): ZIO[R, Nothing, B] =


ZIO(r => self.run(r).fold(failure, success))

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 =>

def provide(r: => R): ZIO[Any, E, A] =


ZIO(_ => self.run(r))

object ZIO:
def environment[R]: ZIO[R, Nothing, R] =
ZIO(r => Right(r))

ZIO Type Aliases


• Sometimes we do not need the full power of the ZIO type and its three
type parameters
• To simplify these cases, ZIO defines a number of useful type aliases
type IO[+E, +A] = ZIO[Any, E, A]
type Task[+A] = ZIO[Any, Throwable, A]
type RIO[-R, +A] = ZIO[R, Throwable, A]
type UIO[+A] = ZIO[Any, Nothing, A]
type URIO[-R, +A] = ZIO[R, Nothing, A]

Comparison to Future
• Unlike ZIO, a Future is a running effect
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

val goShoppingFuture: Future[Unit] =


Future(println("Going to the grocery store"))
• as soon as goShoppingFuture is defined, the effect will begin execution
– that means we cannot delay it; it’s already running
– similarly, we cannot retry it in the event of failure

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] = ???

def parseIntOrZero: Future[Int] =


parseInt.fallBackTo(Future.successful(0))
• Both have the same type even thought the later cannot produce any error
at all !!

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, . . . )

More Effect Constructors


For Pure Computations
• The two most basic ways to convert pure values into ZIO effects are succeed
and ‘fail“
object ZIO:
def succeed[A](a: => A): ZIO[Any, Nothing, A] = ???
def fail[E](e: => E): ZIO[Any, E, Nothing] = ???
• If we have some pure representation of errors:
import scala.util.Try

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] = ???

More Effect Constructors


For Side Effecting Computations
• Earlier we have introduces ZIO.attempt as a constructor that converts
side-effecting code, deferring its evaluation, and translating any exception
thrown into ZIO.fail values.
• If we know the side-effecting code does not throw any exceptions, we cannot
use ZIO.succeed
– we also can use ZIO.succeed if we want to treat them as defects and
not track them

10
object ZIO:
def attempt[A](a: => A): ZIO[Any, Throwable, A] = ???
def succeed[A](a: => A): ZIO[Any, Nothing, A] = ???

More Effect Constructors


Async Callbacks
• Non-blocking code does not synchronously compute and return a value
(blocking the caller)
• It accepts a callback that will be invoked later when the value has been
computed (sometimes this can be hidden inside a Future)
def getUserByIdAsync(id: Int)(cb: Option[String] => Unit): Unit = ???

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)

More Effect Constructors


Async Callbacks
• The constructor to use in this case is ZIO.async
object ZIO:
def async[R, E, A](cb: (ZIO[R, E, A] => Unit) => Any): ZIO[R, E, A] = ???
• As the type can be tricky to understand, let’s see an example:
def getUserById(id: Int): ZIO[Any, None.type, String] =
ZIO.async { callback =>
getUserByIdAsync(id) {
case Some(name) => callback(ZIO.succeed(name))
case None => callback(Zio.fail(None))
}
}
• Note here that in async the callback can only be invoked once
– there is a similar method in ZStream if you need to invoke it more
than once

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"))

val goShoppingZIO: Task[Unit] =


ZIO.fromFuture(implicit ec => goShoppingFuture)

Default ZIO Services


• ZIO provides four diferent default services for all applications
1. Clock, for functionality related to time and scheduling
2. Console, for console input and output
3. System, for systems and environment variables
4. Random, for random number generation
• zio-test, provides a test implementation of these services and you can
also provide other implementations as well

Default ZIO Services


Clock
• The Clock service has methods for obtaining the current time
– currentTime, to get it in the specified TimeUnit
– currentDateTime, as a OffsetDateTime
– nanoTime, to get it in nanoseconds
• It also has a method to sleep
– it does not complete execution until the elapsed time has passes
– it is non-blocking
object Clock:
val nanoTime: ZIO[Any, Nothing, Long]
def sleep(duration: => Duration): ZIO[Any, Nothing, Any]

12
• With sleep we can implement delay:
def delay[R, E, A](zio: ZIO[R, E, A])(duration: Duration) =
Clock.sleep(duration) *> zio

Default ZIO Services


Console
• Before we’ve used ZIO.attempt to wrap procedural use of scala console,
but ZIO incorpores the Console service for that
object Console:
val readLine: ZIO[Any, IOException, String]
def print(line: => String): ZIO[Any, Nothing, Unit]
def printLine(line: => String): ZIO[Any, Nothing, Unit]

Default ZIO Services


System
• This service access the system and environment properties
• Its main methods are
object System:
def env(variable: String): ZIO[Any, SecurityException, Option[String]]
def property(prop: String): ZIO[Any, Throwable, Option[String]]

Default ZIO Services


Random
• This service provides functionality related to random number generation
• It exposes essentially the same interface as scala.util.Random but in a
effectful way

Recursion and ZIO


• ZIO effects are stack safe for arbitrary recursive effects
• So we can write ZIO functions that call themselves to implement any kind
of recursion without blowing up the stack
• Consider the readInt effect which can fail if an integer has not been
entered

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

You might also like