Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional Concurrency with ZIO

Functional Concurrency with ZIO

Itamar Ravid

June 24, 2019
Tweet

More Decks by Itamar Ravid

Other Decks in Programming

Transcript

  1. Itamar Ravid - @itrvd 1

    View full-size slide

  2. Functional Concurrency with
    Itamar Ravid - @itrvd 2

    View full-size slide

  3. Agenda
    • 2 minutes of background
    • An overview of ZIO
    • Map-Reduce with ZIO
    Itamar Ravid - @itrvd 3

    View full-size slide

  4. About me
    • Freelance software developer
    • Doing functional programming, microservices, DevOps
    • Co-organize the Underscore Scala meetup
    • Maintain ZIO, ZIO Streams, ZIO Kafka
    Itamar Ravid - @itrvd 4

    View full-size slide

  5. To be useful,
    a program has to do I/O
    Itamar Ravid - @itrvd 5

    View full-size slide

  6. To be efficient,
    a program should probably use asynchronicity
    Itamar Ravid - @itrvd 6

    View full-size slide

  7. To be fast,
    a program probably needs to use concurrency
    Itamar Ravid - @itrvd 7

    View full-size slide

  8. Scala's answer:
    scala.concurrent.Future[T]
    Itamar Ravid - @itrvd 8

    View full-size slide

  9. Future doesn't block your thread:
    val background = Future {
    Thread.sleep(15000)
    println("Done sleeping!")
    }
    println("I'm not blocked!")
    Itamar Ravid - @itrvd 9

    View full-size slide

  10. Future lets you do things concurrently:
    val firstComputation: Future[Int] = Future(cpuBound(5))
    val secondComputation: Future[Int] = Future(cpuBound(10))
    val zipped: Future[(Int, Int)] =
    firstComputation zip secondComputation
    Itamar Ravid - @itrvd 10

    View full-size slide

  11. The problems with Future
    It's eager. Consider a database request, and a fallback:
    def runRequests(userId: String): Future[Option[User]] = {
    val mainRequest: Future[Option[User]] = getUser(mainDb, userId)
    val fallbackRequest: Future[Option[User]] = getUser(fallbackDb, userId)
    mainRequest.recoverWith {
    case e =>
    log.warn("Main DB failed. Falling back to secondary")
    fallbackRequest
    }
    }
    Itamar Ravid - @itrvd 11

    View full-size slide

  12. Why Future is not good enough
    No cancellation:
    def runRequests(userId: String): Future[Option[User]] =
    Future.firstCompletedOf(
    List(
    getUser(firstReplicaDb, userId),
    getUser(secondReplicaDb, userId)
    )
    )
    Itamar Ravid - @itrvd 12

    View full-size slide

  13. What's ZIO?
    Itamar Ravid - @itrvd 13

    View full-size slide

  14. More concretely
    val program: ZIO[Config, AppError, String]
    • A lazy Future that doesn't necessarily fail with Throwable
    • A purely functional description for asynchronous, concurrent
    programs
    • Something that is kind-of like Config =>
    Either[AppError, String]
    Itamar Ravid - @itrvd 14

    View full-size slide

  15. Creating programs using ZIO
    val constant = ZIO.succeed(42)
    val neverFails = ZIO.effectTotal(fibonacci(4))
    val mayFail = ZIO.effect(readLines("/tmp/dict"))
    Itamar Ravid - @itrvd 15

    View full-size slide

  16. Creating programs using ZIO
    val constant: ZIO[Any, Nothing, Int] =
    ZIO.succeed(42)
    val neverFails: ZIO[Any, Nothing, Int] =
    ZIO.effectTotal(fibonacci(4))
    val mayFail: ZIO[Any, Throwable, List[String]] =
    ZIO.effect(readLines("/tmp/dict"))
    Itamar Ravid - @itrvd 16

    View full-size slide

  17. Adapters
    import scala.util.Try
    val fromTry: ZIO[Any, Throwable, String] = ZIO.fromTry(Try("OK!"))
    Itamar Ravid - @itrvd 17

    View full-size slide

  18. Adapters
    val anEither: Either[String, Boolean] = Right(true)
    val fromEither: ZIO[Any, String, Boolean] =
    ZIO.fromEither(anEither)
    Itamar Ravid - @itrvd 18

    View full-size slide

  19. Adapters
    def loadUser(id: Int)
    (implicit ec: ExecutionContext): Future[Option[User]]
    val fromFuture: ZIO[Any, Throwable, Option[User]] =
    ZIO.fromFuture { implicit ec =>
    loadUser(42)
    }
    Itamar Ravid - @itrvd 19

    View full-size slide

  20. Value transformations
    ZIO features the standard transformations on the value type:
    val user: ZIO[Any, Throwable, User]
    val username: ZIO[Any, Throwable, String] =
    user.map(_.name)
    val group: ZIO[Any, Throwable, Group] =
    user.flatMap(user => getGroup(user.groupId))
    Itamar Ravid - @itrvd 20

    View full-size slide

  21. Error transformations
    We can also run transformations on the error type:
    val user: ZIO[Any, Throwable, User]
    val justTheMessage: ZIO[Any, String, User] =
    user.mapError(_.getMessage)
    val asEither: ZIO[Any, Nothing, Either[Throwable, User]] =
    user.either
    Itamar Ravid - @itrvd 21

    View full-size slide

  22. Error transformations
    We can also run transformations on the error type:
    val user: ZIO[Any, Throwable, User]
    val recovered: ZIO[Any, Nothing, Option[User]] =
    user
    .map(user => Some(user))
    .catchAll(e => ZIO.succeed(None))
    val recovered2: ZIO[Any, Nothing, Option[User]] =
    user.fold(
    e: Throwable => None,
    user => Some(user)
    )
    Itamar Ravid - @itrvd 22

    View full-size slide

  23. Error widening
    Chaining two ZIOs with different error types automatically
    widens the error:
    sealed trait AppError
    case class AuthError(msg: String) extends AppError
    case class DbError(msg: String) extends AppError
    def authenticate: ZIO[Any, AuthError, Token]
    def getUser(token: Token): ZIO[Any, DbError, User]
    Itamar Ravid - @itrvd 23

    View full-size slide

  24. Error widening
    Chaining two ZIOs with different error types automatically
    widens the error:
    val composed: ZIO[Any, AppError, User] =
    for {
    token <- authenticate
    user <- getUser(token)
    } yield user
    No annotations, no compiler helping, nothing!
    Itamar Ravid - @itrvd 24

    View full-size slide

  25. ZIO's error model
    Here's a scenario that Future (and other effect monads) do not
    faithfully represent:
    def getUser(id: Int): ZIO[Any, Throwable, User]
    val twoUsers: ZIO[Any, Throwable, (User, User)] =
    getUser(10) zipPar getUser(20)
    We know what happens if one of them fails. What happens when
    both fail?
    Itamar Ravid - @itrvd 25

    View full-size slide

  26. ZIO's error model
    All errors are preserved properly:
    res12: Exit[Throwable, (User, User)] = Failure(
    Both(
    Fail(java.lang.Exception: Not found: 10),
    Fail(java.lang.Exception: Not found: 20)
    )
    )
    Itamar Ravid - @itrvd 26

    View full-size slide

  27. ZIO's error model
    Itamar Ravid - @itrvd 27

    View full-size slide

  28. ZIO's error model
    Itamar Ravid - @itrvd 28

    View full-size slide

  29. ZIO's error model
    Defects are misbehaved code:
    Task(doSomeIO()).map { result =>
    if (resultIsOk(result)) result
    else throw new RuntimeException() // <- Defect!
    }
    Itamar Ravid - @itrvd 29

    View full-size slide

  30. ZIO's error model
    Itamar Ravid - @itrvd 30

    View full-size slide

  31. ZIO's error model
    Several combinators let us "expose" the full Cause[E]:
    val user: ZIO[Any, Throwable, User]
    val sandboxed: ZIO[Any, Cause[Throwable], User] =
    user.sandbox
    Itamar Ravid - @itrvd 31

    View full-size slide

  32. ZIO's error model
    Several combinators let us "expose" the full Cause[E]:
    val folded =
    user.foldCauseM(
    cause: Cause[Throwable] => // ..,
    user: User => // ..
    )
    val fullExit: ZIO[Any, Nothing, Exit[Throwable, User]] =
    user.run
    Itamar Ravid - @itrvd 32

    View full-size slide

  33. Helpful type aliases
    type UIO[A] = ZIO[Any, Nothing, A]
    type IO[E, A] = ZIO[Any, E, A]
    type Task[A] = ZIO[Any, Throwable, A]
    type TaskR[R, A] = ZIO[R, Throwable, A]
    Itamar Ravid - @itrvd 33

    View full-size slide

  34. Concurrency
    Every sequential execution in ZIO is a fiber.
    We can fork computations using fork:
    val heavyComputation: ZIO[Any, Throwable, Int]
    for {
    fiber: Fiber[Throwable, Int] <- heavyComputation.fork
    // do other stuff
    result <- fiber.join
    } yield result
    Itamar Ravid - @itrvd 34

    View full-size slide

  35. Fine-grained cancellation
    Fibers can be interrupted in order to immediately stop their
    execution.
    for {
    fiber <- heavyComputation.fork
    shouldStop <- makeDecision
    result <- if (shouldWeStop) fiber.interrupt.const(None)
    else fiber.join.map(Some(_))
    } yield result
    Itamar Ravid - @itrvd 35

    View full-size slide

  36. When do fibers get interrupted?
    • In between flatMaps (e.g. "between" for-comprehension
    steps)
    • On constructors that can be interrupted
    Itamar Ravid - @itrvd 36

    View full-size slide

  37. Resource management
    Under fine-grained cancellation, proper resource management is
    critical:
    for {
    fileHandle <- openFile("/tmp/dict")
    lines <- readLines(fileHandle, 3)
    _ <- fileHandle.close
    } yield lines
    We don't want to leak file handles!
    Itamar Ravid - @itrvd 37

    View full-size slide

  38. Resource management
    To do this safely, we can use a bracket:
    val lines: ZIO[Any, Throwable, List[String]] =
    openFile("/tmp/dict")
    .bracket(fileHandle => fileHandle.close) { fileHandle =>
    readLines(handle, 3)
    }
    Itamar Ravid - @itrvd 38

    View full-size slide

  39. Resource management
    Itamar Ravid - @itrvd 39

    View full-size slide

  40. Resource management
    Itamar Ravid - @itrvd 40

    View full-size slide

  41. Resource management
    Itamar Ravid - @itrvd 41

    View full-size slide

  42. Resource management
    Itamar Ravid - @itrvd 42

    View full-size slide

  43. Resource management
    What happens when we need to acquire multiple resources?
    connectRedis.bracket(_.close) { redis =>
    // use Redis
    }
    Itamar Ravid - @itrvd 43

    View full-size slide

  44. Resource management
    What happens when we need to acquire multiple resources?
    connectRedis.bracket(_.close) { redis =>
    connectMySQL.bracket(_.close) { mysql =>
    // use Redis, MySQL
    }
    }
    Itamar Ravid - @itrvd 44

    View full-size slide

  45. Resource management
    What happens when we need to acquire multiple resources?
    connectRedis.bracket(_.close) { redis =>
    connectMySQL.bracket(_.close) { mysql =>
    connectElastic.bracket(_.close) { elastic =>
    // use Redis, MySQL, Elastic ..
    }
    }
    }
    Itamar Ravid - @itrvd 45

    View full-size slide

  46. Resource management
    Itamar Ravid - @itrvd 46

    View full-size slide

  47. Resource management
    We have ZManaged for that:
    case class Resources(redis: Redis, mysql: MySQL, elastic: Elastic)
    val resources: ZManaged[Any, Throwable, Resources] =
    for {
    redis <- ZManaged.make(connectRedis)(_.close)
    mysql <- ZManaged.make(connectMySQL)(_.close)
    elastic <- ZManaged.make(connectElastic)(_.close)
    } yield Resources(redis, mysql, elastic)
    Itamar Ravid - @itrvd 47

    View full-size slide

  48. Resource management
    We have ZManaged for that:
    resources.use { case Resources(redis, mysql, elastic) =>
    // use everything
    }
    Itamar Ravid - @itrvd 48

    View full-size slide

  49. Resource management
    If ZManaged looks obscure, watch my talk about it:
    https://youtu.be/ybXuBXZ8NA0?t=1138
    Itamar Ravid - @itrvd 49

    View full-size slide

  50. Retries and repetitions
    Who's written a function for exponential backoff retrying before?
    Itamar Ravid - @itrvd 50

    View full-size slide

  51. Retries and repetitions
    I can't count how many times I've written this function:
    def retry[A](task: Task[A],
    maxAttempts: Int,
    baseDelay: FiniteDuration): Task[A] = {
    def go(curr: Int) =
    task.catchAll { e =>
    if (curr == maxAttempts) Task.fail(e)
    else go(curr - 1).delay((baseDelay.toSeconds *
    math.pow(2, curr)).seconds)
    }
    go(0)
    }
    Itamar Ravid - @itrvd 51

    View full-size slide

  52. Retries and repetitions
    What happens when we want to add jitter?
    def retry[A](task: Task[A],
    maxAttempts: Int,
    baseDelay: FiniteDuration,
    jitter: Double): Task[A] = {
    def go(curr: Int) =
    task.catchAll { e =>
    if (curr == maxAttempts) Task.fail(e)
    else go(curr - 1).delay((baseDelay.toSeconds *
    math.pow(2, curr) *
    Random.nextDouble(jitter)).seconds)
    }
    go(0)
    }
    Itamar Ravid - @itrvd 52

    View full-size slide

  53. ZSchedule
    With ZIO, the previous combinator reduces to this:
    task.retry(
    ZSchedule.exponential(50.millis).jittered &&
    ZSchedule.recurs(5)
    )
    Itamar Ravid - @itrvd 53

    View full-size slide

  54. ZSchedule
    val schedule: ZSchedule[R, A, B]
    A schedule:
    • Uses an environment of type R,
    • consumes values of type A,
    • computes a delay and outputs values of type B
    Itamar Ravid - @itrvd 54

    View full-size slide

  55. ZSchedule composition
    We can combine schedules using intersection:
    val intersect =
    ZSchedule.exponential(50.millis) &&
    ZSchedule.recurs(5)
    This schedule will repeat using the max delay,
    for as long as both schedules repeat.
    Itamar Ravid - @itrvd 55

    View full-size slide

  56. ZSchedule composition
    by using union:
    val intersect =
    ZSchedule.exponential(50.millis) ||
    ZSchedule.fixed(1.second)
    which will use the min delay, if either
    schedule wants to repeat;
    Itamar Ravid - @itrvd 56

    View full-size slide

  57. ZSchedule composition
    or by sequencing:
    val intersect =
    ZSchedule.recurs(5) andThen
    ZSchedule.spaced(1.second)
    which runs the first schedule, and switches
    to the second schedule.
    Itamar Ravid - @itrvd 57

    View full-size slide

  58. Concurrent data structures
    ZIO features all of the data structures you need to create
    concurrent algorithms.
    Itamar Ravid - @itrvd 58

    View full-size slide

  59. Promise[E, A]
    A Promise[E, A] is a signal you can trigger across threads:
    for {
    promise <- Promise.make[Throwable, String]
    // Wait on the promise in a background fiber:
    fiber <- (for {
    result <- promise.await
    _ <- UIO(println(s"Received $result"))
    } yield ()).fork
    // Resolve it from this fiber:
    _ <- promise.succeed("42")
    // Wait for the fiber to exit:
    _ <- fiber.join
    } yield ()
    Itamar Ravid - @itrvd 59

    View full-size slide

  60. Promise[E, A]
    A Promise[E, A] is a signal you can trigger across threads:
    for {
    promise <- Promise.make[Throwable, String]
    // Wait on the promise in a background fiber:
    fiber <- (for {
    result <- promise.await
    _ <- UIO(println(s"Received $result"))
    } yield ()).fork
    // Make it fail:
    _ <- promise.fail(new Exception)
    // Wait for the fiber to exit:
    _ <- fiber.join
    } yield ()
    Itamar Ravid - @itrvd 60

    View full-size slide

  61. Ref[A]
    A Ref[A] is a mutable reference you can change across threads:
    for {
    counter <- Ref.make(0)
    incrementer <- counter.update(_ + 1)
    .repeat(Schedule.spaced(1.second))
    .fork
    _ <- UIO.sleep(5.seconds)
    result <- counter.get
    } yield result
    Itamar Ravid - @itrvd 61

    View full-size slide

  62. Queue[A]
    A high-performance, lock-free concurrent queue:
    trait Queue[A] {
    // ..
    def offer(x: A): UIO[Boolean]
    def take: UIO[A]
    // ..
    }
    Itamar Ravid - @itrvd 62

    View full-size slide

  63. Queue[A]
    Here's, for example, a fiber copying items between queues:
    for {
    input <- Queue.bounded[Int](16)
    output <- Queue.bounded[Int](16)
    _ <- input.offerAll(List.fill(16)(10))
    fiber <- (for {
    element <- input.take
    _ <- output.offer(element)
    } yield ()).forever.fork
    _ <- UIO.sleep(1.second)
    _ <- fiber.interrupt
    result <- output.takeAll
    } yield result
    Itamar Ravid - @itrvd 63

    View full-size slide

  64. Semaphore
    A counter we can use to limit access to a resource:
    for {
    permits <- Semaphore.make(10)
    expensiveComputation =
    permits.withPermit {
    compute()
    }
    computations <- ZIO.collectAllPar(List.fill(50)(expensiveComputation))
    } yield computations
    Itamar Ravid - @itrvd 64

    View full-size slide

  65. Semaphore
    Or, more shortly:
    ZIO.collectAllParN(10) {
    List.fill(50)(compute())
    }
    Itamar Ravid - @itrvd 65

    View full-size slide

  66. Async traces
    Say you're using Future to write an HTTP crawler:
    def httpCall(url: String): Future[String]
    def first = httpCall("first:8080")
    def second = httpCall("second:8080")
    val result: Future[(String, String)] = first zip second
    Itamar Ravid - @itrvd 66

    View full-size slide

  67. Async traces
    Your job is running in production and hits a failure.
    This is your stack trace:
    java.lang.RuntimeException
    at test.Crawler$.$anonfun$httpCall$1(bla.scala:8)
    at scala.concurrent.Future$.$anonfun$apply$1(Future.scala:660)
    at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:430)
    at scala.concurrent.BatchingExecutor$AbstractBatch.runN(BatchingExecutor.scala:134)
    at scala.concurrent.BatchingExecutor$AsyncBatch.apply(BatchingExecutor.scala:163)
    at scala.concurrent.BatchingExecutor$AsyncBatch.apply(BatchingExecutor.scala:146)
    at scala.concurrent.BlockContext$.usingBlockContext(BlockContext.scala:107)
    at scala.concurrent.BatchingExecutor$AsyncBatch.run(BatchingExecutor.scala:154)
    at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
    at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
    at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
    at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
    at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
    Which call failed???
    Itamar Ravid - @itrvd 67

    View full-size slide

  68. Async traces
    With ZIO, you're getting something much more useful:
    A checked error was not handled.
    java.lang.RuntimeException
    //
    Fiber:0 was supposed to continue to:
    a future continuation at zio.ZIO.zipWith(ZIO.scala:788)
    Fiber:0 execution trace:
    at ammonite.$file.bla$.httpCall(bla.scala:6)
    at ammonite.$file.bla$.google(bla.scala:8)
    Itamar Ravid - @itrvd 68

    View full-size slide

  69. A full example
    Let's see everyone's favourite example!
    Itamar Ravid - @itrvd 69

    View full-size slide

  70. !
    Map / Reduce
    Itamar Ravid - @itrvd 70

    View full-size slide

  71. Wordcount over several files
    We will:
    • List files in a directory
    • Enqueue all paths into a queue
    • Create 4 worker fibers:
    • dequeue paths from the queue and count the words
    Itamar Ravid - @itrvd 71

    View full-size slide

  72. Listing files
    This is a good example of using blocking effects:
    import java.nio.file.{Files, Paths}
    import scala.collection.JavaConverters._
    import zio.blocking._
    def listFiles(dir: String): ZIO[Blocking, Throwable, List[Path]] =
    effectBlocking {
    Files.list(Paths.get(dir)).iterator.asScala.toList
    }
    Itamar Ravid - @itrvd 72

    View full-size slide

  73. Count words in a file
    Another blocking I/O effect:
    def readContents(path: Path): ZIO[Blocking, Throwable, String] =
    effectBlocking {
    new String(
    Files.readAllBytes(path),
    "UTF-8"
    )
    }
    Itamar Ravid - @itrvd 73

    View full-size slide

  74. Put them together:
    def countAllWords(dir: String): ZIO[Blocking, Throwable, Long] =
    for {
    paths <- listFiles(dir)
    counts <- ZIO.foreach(paths) { path =>
    readContents(path).map { contents =>
    contents.split(' ').size
    }
    }
    } yield counts.sum
    Itamar Ravid - @itrvd 74

    View full-size slide

  75. Actually making this map/reduce
    def mapReduce[A, B](dir: String)
    (map: String => A)
    (z: B)(reduce: (B, A) => B): ZIO[Blocking, Throwable, B] =
    for {
    paths <- listFiles(dir)
    mapped <- ZIO.foreach(paths) { path =>
    readContents(path).map { contents =>
    map(contents)
    }
    }
    } yield mapped.foldLeft(z)(reduce)
    def countAllWords(dir: String) = mapReduce(dir)(_.split(' ').size)(0L)(_ + _)
    Itamar Ravid - @itrvd 75

    View full-size slide

  76. Use workers to process the data
    def mapReduce[A, B](dir: String, workers: Int)
    (map: String => A)
    (z: B)(reduce: (B, A) => B): ZIO[Blocking, Throwable, B] =
    for {
    paths <- listFiles(dir)
    mapped <- ZIO.foreachParN(workers)(paths) { path =>
    readContents(path).map { contents =>
    map(contents)
    }
    }
    } yield mapped.foldLeft(z)(reduce)
    Itamar Ravid - @itrvd 76

    View full-size slide

  77. Set things up for directory monitoring
    Our map worker:
    def createMapWorker[A](inputQueue: Queue[Path], reduceQueue: Queue[A])(map: String => A) =
    (for {
    path <- inputQueue.take
    contents <- readContents(path)
    a = map(contents)
    _ <- reduceQueue.offer(a)
    } yield ()).forever.fork
    Itamar Ravid - @itrvd 77

    View full-size slide

  78. Set things up for directory monitoring
    Our reduce worker:
    def createReduceWorker[A, B](inputQueue: Queue[A], outputQueue: Queue[B], latest: Ref[B])
    (reduce: (B, A) => B) =
    (for {
    b <- latest.get
    a <- inputQueue.take
    newB = reduce(b, a)
    _ <- latest.set(newB)
    _ <- outputQueue.offer(newB)
    } yield ()).forever.fork
    Itamar Ravid - @itrvd 78

    View full-size slide

  79. Set things up for directory monitoring
    Launch everything:
    def mapReduce[A, B](inputQueue: Queue[Path], reduceQueue: Queue[A],
    outputQueue: Queue[B], workers: Int)
    (map: String => A)
    (z: B)(reduce: (B, A) => B): ZIO[Blocking, Throwable, Unit] =
    for {
    mapFibers <- ZIO.collectAll(
    List.fill(workers)(createMapWorker(inputQueue, reduceQueue)(map))
    )
    reduceFiber <- for {
    bRef <- Ref.make(z)
    fiber <- createReduceWorker(reduceQueue, outputQueue, bRef)(reduce)
    } yield fiber
    _ <- Fiber.joinAll(reduceFiber :: mapFibers)
    } yield ()
    Itamar Ravid - @itrvd 79

    View full-size slide

  80. Set things up for directory monitoring
    And we need something to print intermediate results:
    def printer(queue: Queue[Long]) =
    (for {
    count <- queue.take
    _ <- UIO(println("Current count: $count"))
    } yield ()).forever.fork
    Itamar Ravid - @itrvd 80

    View full-size slide

  81. Set things up for directory monitoring
    And our final function for running:
    def countAllWords(dir: String, workers: Int) =
    for {
    inputQueue <- Queue.bounded[Path](16)
    reduceQueue <- Queue.bounded[Long](16)
    outputQueue <- Queue.bounded[Long](16)
    paths <- listFiles(dir)
    _ <- inputQueue.offerAll(paths)
    _ <- mapReduce(inputQueue, reduceQueue, outputQueue, workers)(
    _.split(' ').size)(0L)(_ + _).fork
    printerFib <- printer(outputQueue)
    _ <- printerFib.await
    } yield ()
    Itamar Ravid - @itrvd 81

    View full-size slide

  82. That's it!
    But we haven't covered:
    • The R type parameter
    • STM - a super-easy way to construct concurrent applications
    • Fiber-local variables - a pure equivalent to thread-locals
    • ZStreams - ZIO's streaming module
    Itamar Ravid - @itrvd 82

    View full-size slide

  83. Interested to learn more?
    • https://zio.dev/
    • https://github.com/zio/zio
    • https://gitter.im/zio/core
    Itamar Ravid - @itrvd 83

    View full-size slide

  84. Interested to learn more?
    We've got John de Goes coming to Israel to teach a workshop
    about ZIO,
    graciously hosted by IronSource!
    https://www.eventbrite.com/e/advanced-async-and-concurrent-
    programming-with-zio-by-john-a-de-goes-tickets-63867611746
    Itamar Ravid - @itrvd 84

    View full-size slide