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 success

More 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 <*> b

Zip 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 <&> b

Zip 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)
  .exitCode

Construct 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?