ZIO
A Functional Effect System for Scala
Marc Saegesser
October 22, 2020
About me
- Software developer for about 35 years
- Scala developer for about 9 years
- Frequent CASE presenter
- Not ZIO contributor
- A ZIO user
Goals
- History and motivation for ZIO
- Tour of ZIO
- Get you interested
- Get you started
Some history

Pandora's Box
War
Greed
Envy
Famine
Death
Poverty
von Neumann's Box

I/O
Side Effects
Errors
Concurrency
Resource Management
Asynchronous

Pandora's Box
Hope
von Neumann's Box

Hope?
von Neumann's Box
What is the analog of Pandoran Hope?
¯\_(ツ)_/¯
What about ZIO?
- ZIO is an effect system
- Aimed at taming and handling these effects
- Tools for concurrent and asychcronous programming
- Allocating and release resources
- Error handling
The 1990s


WWW

Haskell

Haskell
- Lazy and purely functional
- Purity implies no side effects
- But we need side effects to be useful!
- Game over?
If you can't win, cheat.

Change the rules so that success is possible.
Split the universe in two
- A pure functional part
- Don't do side effects, describe them
- IO a
- A value of type IO a is an “action” that, when performed, may do some input/output, before delivering a value of type a.
- We write pure code that combine IO values that specify our entire application
- An impure runtime that performs effects
- The runtime knows about the outside world
- Performs the actions described by IO values
- Baked into Haskell executables
module Main (main) where
main :: IO ()
main = putStrLn "Hello, World!"
This is important
- Programs are values (IO a)
- These values describe our program
- Make larger programs by combining smaller ones
- Combinators are pure functions
- A runtime performs our actions
ZIO
- An IO-like construct for Scala
- Based on the ideas of Haskell's IO
- Completely different
- Lots of combinators
- sequencing
- looping
- concurrency
- error handling
- A highly performant runtime
- Fibers (Green threads)
- Inexpensive
- Interruptible
ZIO[R, E, A]A ZIO is an action, that when performed...
Succeeds with a value of type A
Fails with a value of type E
Requires an environment of type R
import zio._
import console._
object Main extends App {
override def run(args: List[String]) =
putStrLn("Hello, world!").exitCode
}
Hello World! from ZIO
import zio._
import console._
object Main extends App {
override def run(args: List[String]): ZIO[Console, Nothing, ExitCode] =
putStrLn("Hello, world!").exitCode
}
// console.putStrLn(line: => String): ZIO[Console, Nothing, Unit]Hello World! from ZIO
object Prompt extends App {
val program: ZIO[Console, IOException, Unit] =
for {
_ <- putStr("What's your name?: ")
n <- getStrLn
_ <- putStrLn(s"Hello, $n!")
} yield ()
override def run(args: List[String]): ZIO[Console, Nothing, ExitCode] =
program.exitCode
}
// console.getStrLn: ZIO[Console, IOException, String]getStrLn can fail with IOException
exitCode transforms ZIO[R, E, A] to ZIO[R, Nothing, ExitCode]
object Prompt2 extends App {
val program: ZIO[Console, IOException, Unit] =
for {
_ <- putStr("What's your name?: ")
n <- getStrLn
_ <- putStrLn(s"Hello, $n!")
} yield ()
override def run(args: List[String]): ZIO[Console, Nothing, ExitCode] =
program
.catchAll { e => putStrLn("") *> putStrLn(s"Input failed due to '${e.getMessage}'") }
.exitCode
}
// catchAll[R1 <: R, E2, A1 >: A](h: (E) => ZIO[R1, E2, A1]): ZIO[R1, E2, A1]Recover from all errors
object PromptRepeat extends App {
val program: ZIO[Console, IOException, Unit] =
for {
_ <- putStr("What's your name?: ")
n <- getStrLn
_ <- putStrLn(s"Hello, $n!")
} yield ()
override def run(args: List[String]): ZIO[Console, Nothing, ExitCode] =
program
.forever // ZIO[R, E, A] => ZIO[R, E, Nothing]
.catchSome { case _: EOFException => putStrLn("") *> putStrLn("Bye")}
.exitCode
}val a: ZIO[Any, Throwable, Int] = ZIO.fail(new IOException("Bang!"))
a.orElse { ZIO.succeed(42) }
a.mapError { _.getMessage }
a.either // ZIO[R, E, A] => ZIO[R, Nothing, Either[E, A]]
a.eventually // Repeat until successMore ways to handle errors
val a: ZIO[Console, Nothing, String] = putStrLn("A").as("A")
val b: ZIO[Console, Nothing, Int] = putStrLn("1").as(1)
val f: ZIO[Any, String, Nothing] = ZIO.fail("Bang!")
val aZipB: ZIO[Console, Nothing, (String, Int)] = a <*> b
val aZipBLeft: ZIO[Console, Nothing, String] = a <* b
val aZipBRight: ZIO[Console, Nothing, Int] = a *> b
val aZipFail: ZIO[Console, String, (String, Nothing)] = a <*> f
val failZipB: ZIO[Console, String, (Nothing, Int)] = f <*> bZip two ZIOs into a single ZIO
val a: ZIO[Console, Nothing, String] = putStrLn("A").as("A")
val b: ZIO[Console, Nothing, Int] = putStrLn("1").as(1)
val f: ZIO[Any, String, Nothing] = ZIO.fail("Bang!")
val aParB: ZIO[Console, Nothing, (String, Int)] = a <&> b
val aParBLeft: ZIO[Console, Nothing, String] = a <& b
val aParBRight: ZIO[Console, Nothing, Int] = a &> b
val aParFail: ZIO[Console, String, (String, Nothing)] = a <&> f
val failParB: ZIO[Console, String, (Nothing, Int)] = f <&> bZip two ZIOs in parallel
val a: ZIO[Console, Nothing, String] = putStrLn("A").as("A")
val b: ZIO[Console, Nothing, String] = putStrLn("B").as("B")
val c: ZIO[Console, Nothing, String] = putStrLn("C").as("C")
val f: ZIO[Any, String, Nothing] = ZIO.fail("Bang!")
val x: URIO[Console, String] = a.race(b)
val y: URIO[Console, String] = a.raceAll(List(b, c, f))
val z: URIO[Console, Either[String, String] = a.raceEither(b)Race ZIOs returning the first to succeed
val a: Task[BufferedSource] =
ZIO.effect {
Source.fromFile("/tmp/fubar")
}
// type Task[A] = ZIO[Nothing, Throwable, A]
// ZIO.effect[A](effect: => A): Task[A]Create a ZIO from Scala code with side effects
val a: UIO[Unit] =
ZIO.effectTotal {
logger.info("Hello, world!")
}
// type UIO[A] = ZIO[Nothing, Nothing, A]
// ZIO.effectTotal[A](effect: => A): UIO[A]
Create a ZIO from Scala code that can't fail
val a: IO[String, String] =
ZIO.fromEither {
basicRequest
.get(uri"http://iscaliforniaonfire.com").send()
.body
}
// type IO[E, A] = ZIO[Nothing, E, A]
// ZIO.fromEither[E, A](v: => Either[E, A]): IO[E, A]Create a ZIO from an Either
val a: RIO[Blocking, ConsumerRecords] =
blocking.effectBlockingCancelable {
consumer.poll(60.seconds)
} { consumer.wakeup() }
// type RIO[R, A] = ZIO[R, Throwable, A]
// effectBlockingCancelable(effect: => A)
// (cancel: UIO[Unit]): RIO[Blocking, A]Create a ZIO for a blocking effect
def repeatN(n: Int): ZIO[R, E, A]
def repeatUntil(f: (A) ⇒ Boolean): ZIO[R, E, A]
def repeatUntilM[R1 <: R](f: (A) ⇒ URIO[R1, Boolean]): ZIO[R1, E, A]
def repeatWhile(f: (A) ⇒ Boolean): ZIO[R, E, A]
def repeatWhileM[R1 <: R](f: (A) ⇒ URIO[R1, Boolean]): ZIO[R1, E, A]Repeating a ZIO
zio.forever: ZIO[R, E, Nothing]Repeat an io continuously until failure
Repeat an io continuously until success
zio.eventually: ZIO[R, Nothing, A]zio.repeat[R1 <: R, B](schedule: Schedule[R1, A, B]): ZIO[R1 with Clock, E, B]Repeat an io on a schedule or until failure
Repeat an io on a schedule or until success
zio.retry[R1 <: R, S](policy: Schedule[R1, E, S]): ZIO[R1 with Clock, E, A]A Schedule[R, I, O] represents a sequence of intervals
At each interval the schedule consumes an I, determines whether or not to recur and if so produces a value of type O
once: Schedule[Any, Any, Unit]
forever: Schedule[Any, Any, Long]
recurs(n: Int): Schedule[Any, Any, Long]
recurUntil[A](f: (A) => Boolean): Schedule[Any, A, A]
recurWhile[A](f: (A) => Boolean): Schedule[Any, A, A]
fixed(interval: Duration): Schedule[Any, Any, Long]
spaced(duration: Duration): Schedule[Any, Any, Long]
exponential(base: Duration, factor: Double=2.0): Schedule[Any, Any, Duration]
linear(base: Duration): Schedule[Any, Any, Duration]Primative Schedules
Schedule Combinators
Schedule.fixed(60.seconds) && Schedule.recurs(10)Fixed interval for 10 repetitions
Schedule.exponential(500.millis) || Schedule.fixed(1.minute)Exponential backoff while less than 1 minute then fixed 1 minute interval
(Schedule.exponential(500.millis) || Schedule.fixed(1.minute)).jittered
&& Schedule.recurs(100)Jittered exponential backoff while less than 1 minute then fixed interval for 100 total repetitions
(Schedule.spaced(1.second) && Schedule.recurs(5)) andThen
(Schedule.spaced(5.seconds) && Schedule.recurs(5))5 intervals with 1 second spacing then 5 intervals with 5 second spacing
val program =
for {
status <- statusReporter.fork
http <- httpServer.fork
data <- dataLoop.fork
_ <- Fiber.joinAll(List(status, http, data))
} yield ()Create fibers with fork
val dataLoop: ZIO[Blocking with Clock, Throwable, Long] =
ZIO.bracket(DataSource())(_.close().ignore) { source =>
(for {
ds <- source.fetch(30.seconds)
ws <- mkWidgets(ds)
_ <- (publishWidgets(ws) <* source.commit()).uninterruptible
} yield ()).repeat(Schedule.forever)
}Manage resources with Bracket
import zio._
object module {
type Module = Has[Module.Service]
object Module {
trait Service {
def doSomething(i: Int): ZIO[Nothing, IOException, String]
}
val live: ZLayer[Any, Nothing, Module] =
new ZLayer.succeed(
new Module.Service {
def doSomething(i: Int): ZIO[Nothing, IOException, String] = ???
}
)
}
def doSomething(i: Int): ZIO[Module, IOException, String] =
ZIO.accessM(_.get.doSomething(i))
}Modules
val env = Module1.live +++ Console.live +++ (Module2.live >>> Module3.live)
program
.provideLayer(env)
.exitCodeConstruct an environment using layer combinators
TArray[A]
TMap[K, V]
TPriorityQueue[A]
TPromise[E, A]
TQueue[A]
TSemaphore
TSet[A]Software Transactional Memory
High Level Concurrency Tools
ZRef
ZRef.set(a: A): IO[E, Unit]
ZRef.get: IO[E, A]Interop
- Cats Effect
- Monix
- Java
- Reactive Streams
Conclusions
- Why? What does this actually get us?
- Easier and faster to construct correct programs
- Pure functional programming with immutable data
- Explicit success and failure types
- Combinators
- Runtime
- Much better concurrent performance
- Resource safety for errors and interruption
- Costs
- Learning curve
- Discipline
- Migration of existing code
- Exploring new (to us) ways to creating software
Questions
Demo?
ZIO: A Functional Effect System for Scala
By Marc Saegesser
ZIO: A Functional Effect System for Scala
- 517