# ZIO > Type-safe, composable asynchronous and concurrent programming for Scala. This file contains all documentation content in a single document following the llmstxt.org standard. ## Basic Concurrency ZIO is a highly concurrent framework, powered by _fibers_, which are lightweight virtual threads that achieve massive scalability compared to threads, augmented with resource-safe cancellation, which powers many features in ZIO. This powerful concurrency model lets you do more with less, achieving highly-scalable, ultra low-latency applications that are globally efficient and resource-safe. In this section, you will learn the basics of fibers, and become acquainted with some of the powerful high-level operators that are powered by fibers. ## Fibers All effects in ZIO are executed by _some_ fiber. If you did not create the fiber, then the fiber was created by some operation you are using (if the operation is concurrent or parallel), or by the ZIO runtime system. Even if you only write "single-threaded" code, with no parallel or concurrent operations, there will be at least one fiber: the "main" fiber that executes your effect. Like operating system-level threads, ZIO fibers have a well-defined lifecycle, defined by the effect they are executing. Every fiber exits with failure or success, depending on whether the effect it is executing fails or succeeds. Also like operating system threads, ZIO fibers have unique identities, stacks (including stack traces), local state, and a status (such as _done_, _running_, or _suspended_). Compared to operating system threads, ZIO fibers: - Consume almost no memory - Have dynamic stacks that grow and shrink - Don't waste operating system threads with blocking operations - Can be safely interrupted at any point in time - Are strongly typed - Let you query them to discover their children - Will be garbage collected automatically if they are suspended and cannot be reactivated These make fibers a superior choice for building modern applications. Fibers are scheduled onto operating system threads by the ZIO runtime. Because fibers cooperatively yield to each other, ZIO fibers always execute concurrently, even when running in a single-threaded environment like JavaScript (or the JVM, when ZIO is configured with one work thread). ### The Fiber Data Type The `Fiber` data type in ZIO represents a "handle" on the execution of an effect. The `Fiber` data type is most similar to Scala's `Future` data type, which represents a "handle" on a running asynchronous operation. The `Fiber[E, A]` data type in ZIO has two type parameters: - **`E` Failure Type**. The fiber may fail with a value of this type. - **`A` Success Type**. The fiber may succeed with a value of this type. Fibers do not have an `R` type parameter, because fibers only execute effects that have already had their requirements provided to them. ### Forking Effects The most fundamental way of creating a fiber is to take an existing effect and _fork_ it. Conceptually, _forking_ an effect begins executing the effect on a new fiber, giving you a reference to the newly-created fiber. The following code creates a single fiber using `fork`, which executes `fib(100)` independently of the main fiber: ```scala def fib(n: Long): UIO[Long] = ZIO.suspendSucceed { if (n <= 1) ZIO.succeed(n) else fib(n - 1).zipWith(fib(n - 2))(_ + _) } val fib100Fiber: UIO[Fiber[Nothing, Long]] = for { fiber <- fib(100).fork } yield fiber ``` ### Joining Fibers One of the methods on `Fiber` is `Fiber#join`, which returns an effect. The effect returned by `Fiber#join` will succeed or fail as per the fiber: ```scala for { fiber <- ZIO.succeed("Hi!").fork message <- fiber.join } yield message ``` When a parent fiber joins a child fiber, it will succeed or fail in the same way as the child fiber, and the local states of the fibers will be merged. ### Awaiting Fibers Another method on `Fiber` is `Fiber#await`, which returns an effect containing an `Exit` value, which provides full information on how the fiber completed. ```scala for { fiber <- ZIO.succeed("Hi!").fork exit <- fiber.await } yield exit ``` Awaiting the exit values of fibers is different than joining them, because awaiting will not tie the fate of the parent fiber to that of the child fiber, and nor will it attempt to merge the local states of the fibers. ### Interrupting Fibers A fiber whose result is no longer needed may be _interrupted_, which immediately terminates the fiber, safely releasing all resources by running all finalizers. Like `await`, `Fiber#interrupt` returns an `Exit` describing how the fiber completed. ```scala for { fiber <- ZIO.succeed("Hi!").forever.fork exit <- fiber.interrupt } yield exit ``` By design, the effect returned by `Fiber#interrupt` does not resume until the fiber has completed, which helps ensure your code does not spin up new fibers until the old one has terminated. If this behavior (often called "back-pressuring") is not desired, you can `ZIO#fork` the interruption itself into a new fiber: ```scala for { fiber <- ZIO.succeed("Hi!").forever.fork _ <- fiber.interrupt.fork // I don't care! } yield () ``` There is a shorthand for background interruption, which is the method `Fiber#interruptFork`. ### Composing Fibers ZIO lets you compose fibers with `Fiber#zip` or `Fiber#zipWith`. These methods combine two fibers into a single fiber that produces the results of both. If either fiber fails, then the composed fiber will fail. ```scala for { fiber1 <- ZIO.succeed("Hi!").fork fiber2 <- ZIO.succeed("Bye!").fork fiber = fiber1.zip(fiber2) tuple <- fiber.join } yield tuple ``` Another way fibers compose is with `Fiber#orElse`. If the first fiber succeeds, the composed fiber will succeed with its result; otherwise, the composed fiber will complete with the exit value of the second fiber (whether success or failure). ```scala for { fiber1 <- ZIO.fail("Uh oh!").fork fiber2 <- ZIO.succeed("Hurray!").fork fiber = fiber1.orElse(fiber2) message <- fiber.join } yield message ``` ## Parallelism ZIO provides parallel versions of many methods, which are named with a `Par` suffix that helps you identify opportunities to parallelize your code. For example, the ordinary `ZIO#zip` method zips two effects together sequentially. But there is also a `ZIO#zipPar` method, which zips two effects together in parallel. The following table summarizes some of the sequential operations and their corresponding parallel versions: | **Description** | **Sequential** | **Parallel** | | -----------------------------: | :---------------: | :------------------: | | Zips two effects into one | `ZIO#zip` | `ZIO#zipPar` | | Zips two effects into one | `ZIO#zipWith` | `ZIO#zipWithPar` | | Zips multiple effects into one | `ZIO#tupled` | `ZIO#tupledPar` | | Collects from many effects | `ZIO.collectAll` | `ZIO.collectAllPar` | | Effectfully loop over values | `ZIO.foreach` | `ZIO.foreachPar` | | Reduces many values | `ZIO.reduceAll` | `ZIO.reduceAllPar` | | Merges many values | `ZIO.mergeAll` | `ZIO.mergeAllPar` | Because all these parallel operators return all the results, if any effect being parallelized fails, ZIO will automatically cancel the other running effects, as their results will not be used. If the fail-fast behavior is not desired, potentially failing effects can be first converted into infallible effects using the `ZIO#either` or `ZIO#option` methods. ## Racing ZIO lets you race multiple effects concurrently, returning the first successful result: ```scala for { winner <- ZIO.succeed("Hello").race(ZIO.succeed("Goodbye")) } yield winner ``` If you want the first success **or** failure, rather than the first success, then you can use `left.either.race(right.either)`, for any two effects `left` and `right`. ## Timeout ZIO has resource-safe, compositional timeouts that work on "small" effects, such as querying a database or calling a cloud API, or even "large" effects, such as running a streaming pipeline or fully handling a web request. ZIO lets you timeout effects using the `ZIO#timeout` method, which returns a new effect that succeeds with an `Option` value. A value of `None` indicates the timeout elapsed before the effect completed. ```scala ZIO.succeed("Hello").timeout(10.seconds) ``` If an effect times out, then instead of continuing to execute in the background, it will be interrupted, for automatic efficiency. ## Next Steps If you are comfortable with basic concurrency, the next step is to learn about [running effects](running-effects.md). --- ## Basic Operations Like the `String` data type, as well as the collection data types in Scala (such as `List`, `Map`, and `Set`), ZIO effects are _immutable_, and cannot be changed. In order to transform or combine ZIO effects, you can use the methods on the ZIO data type, which return _new effects_, with the specified transformations or combinations applied to them. There are two categories of methods on the ZIO data type: - **Transformations**. Transformation functions alter an effect in some well-defined way, allowing you to customize runtime behavior. For example, calling `effect.timeout(60.seconds)` on an effect returns a new effect, which, when executed, will apply a timeout to the original effect. - **Combinations**. Combination functions combine two or more effects together in a single effect. For example, calling `effect1.orElse(effect2)` combines two effects in such a fashion that the returned effect, when executed, will first execute the left hand side, and if that fails, it will then execute the right hand side. This lets you specify a fallback effect in case a primary effect fails. ## Mapping If you have an effect that succeeds with some value, you can use `ZIO#map` to obtain a new effect, which will transform the value using the function you provide. ```scala val succeeded: ZIO[Any, Nothing, Int] = ZIO.succeed(21).map(_ * 2) ``` In a similar fashion, you can transform an effect that has one error to an effect with a different error using the `ZIO#mapError` method, which requires you supply a function to do the conversion: ```scala val failed: ZIO[Any, Exception, Unit] = ZIO.fail("No no!").mapError(msg => new Exception(msg)) ``` Note that mapping the error or success value of an effect does not change _whether or not_ the effect fails or succeeds. This is similar to how mapping over Scala's `Either` data type does not change whether the `Either` is `Left` or `Right`. ## Chaining You can execute two effects sequentially with the `flatMap` method. The `flatMap` method requires that you pass a callback, which will receive the success value of the first effect, and must return a second effect, which depends on this value: ```scala val sequenced: ZIO[Any, IOException, Unit] = Console.readLine.flatMap(input => Console.printLine(s"You entered: $input")) ``` If the first effect fails, the callback passed to `flatMap` will never be invoked, and the effect returned by `flatMap` will also fail. In _any_ chain of effects created with `flatMap`, the first failure will short-circuit the whole chain, just like throwing an exception will prematurely exit a sequence of statements. ## For Comprehensions Because the ZIO data type supports both `flatMap` and `map`, you can use Scala's _for comprehensions_ to build imperative effects: ```scala val program: ZIO[Any, IOException, Unit] = for { _ <- Console.printLine("Hello! What is your name?") name <- Console.readLine _ <- Console.printLine(s"Hello, ${name}, welcome to ZIO!") } yield () ``` _For comprehensions_ provide a procedural syntax for creating chains of effects, and are the fastest way for most programmers to get up to speed using ZIO. ## Zipping You can combine two effects into a single effect with the `ZIO#zip` method. The method returns an effect that will execute the left effect first, followed by the right effect, and which will place both success values into a tuple: ```scala val zipped: ZIO[Any, Nothing, (String, Int)] = ZIO.succeed("4").zip(ZIO.succeed(2)) ``` In any `zip` operation, if either the left or right-hand side fails, the composed effect will fail, because _both_ values are required to construct the tuple. If the left side fails, the right side will not be executed at all. Sometimes, when the success value of an effect is not useful (for example, if it is `Unit`), it can be more convenient to use the `ZIO#zipLeft` or `ZIO#zipRight` functions, which first perform a `zip` and then map over the tuple to discard one side or the other: ```scala val zipRight1: ZIO[Any, IOException, String] = Console.printLine("What is your name?").zipRight(Console.readLine) ``` The `zipRight` and `zipLeft` functions have symbolic aliases, known as `*>` and `<*`, respectively. Some developers find these operators easier to read: ```scala val zipRight2: ZIO[Any, IOException, String] = Console.printLine("What is your name?") *> Console.readLine ``` ## Next Steps If you are comfortable with the basic operations on ZIO effects, the next step is to learn about [error handling](handling-errors.md). --- ## Creating Effects This section explores some of the common ways to create ZIO effects from values, from computations, and from common Scala data types. ## From Values Using the `ZIO.succeed` method, you can create an effect that, when executed, will succeed with the specified value: ```scala val s1 = ZIO.succeed(42) ``` The `succeed` method takes a so-called _by-name parameter_, which ensures that if you pass the method some code to execute, that this code will be stored inside the ZIO effect so that it can be managed by ZIO, and benefit from features like retries, timeouts, and automatic error logging. ## From Failure Values Using the `ZIO.fail` method, you can create an effect that, when executed, will fail with the specified value: ```scala val f1 = ZIO.fail("Uh oh!") ``` For the `ZIO` data type, there is no restriction on the error type. You may use strings, exceptions, or custom data types appropriate for your application. Many applications will model failures with classes that extend `Throwable` or `Exception`: ```scala val f2 = ZIO.fail(new Exception("Uh oh!")) ``` ## From Scala Values Scala's standard library contains a number of data types that can be converted into ZIO effects. ### Option An `Option` can be converted into a ZIO effect using `ZIO.fromOption`: ```scala val zoption: IO[Option[Nothing], Int] = ZIO.fromOption(Some(2)) ``` The error type of the resulting effect is `Option[Nothing]`, signifying that if such an effect fails, it will fail with the value `None` (which has type `Option[Nothing]`). You can transform a failure into some other error value using `orElseFail`, one of many methods that ZIO provides for error management: ```scala val zoption2: ZIO[Any, String, Int] = zoption.orElseFail("It wasn't there!") ``` ZIO has a variety of other operators designed to make interfacing with `Option` code easier. In the following advanced example, the operators `some` and `asSomeError` are used to make it easier to interface with methods returning `Option`, similar to the `OptionT` type in some Scala libraries. ```scala val maybeId: ZIO[Any, Option[Nothing], String] = ZIO.fromOption(Some("abc123")) def getUser(userId: String): ZIO[Any, Throwable, Option[User]] = ??? def getTeam(teamId: String): ZIO[Any, Throwable, Team] = ??? val result: ZIO[Any, Throwable, Option[(User, Team)]] = (for { id <- maybeId user <- getUser(id).some team <- getTeam(user.teamId).asSomeError } yield (user, team)).unsome ``` ### Either An `Either` can be converted into a ZIO effect using `ZIO.fromEither`: ```scala val zeither: ZIO[Any, Nothing, String] = ZIO.fromEither(Right("Success!")) ``` The error type of the resulting effect will be that of the `Left` case, while the success type will be that of the `Right` case. ### Try A `Try` value can be converted into a ZIO effect using `ZIO.fromTry`: ```scala val ztry = ZIO.fromTry(Try(42 / 0)) ``` The error type of the resulting effect will always be `Throwable` because `Try` can only fail with values of type `Throwable`. ### Future A Scala `Future` can be converted into a ZIO effect using `ZIO.fromFuture`: ```scala lazy val future = Future.successful("Hello!") val zfuture: ZIO[Any, Throwable, String] = ZIO.fromFuture { implicit ec => future.map(_ => "Goodbye!") } ``` The function passed to `fromFuture` is provided an `ExecutionContext`, which allows ZIO to manage where the `Future` runs (of course, you can ignore this `ExecutionContext`). The error type of the resulting effect will always be `Throwable`, because `Future` values can only fail with values of type `Throwable`. ## From Code ZIO can convert any code (such as a call to some method) into an effect, whether that code is so-called _synchronous_ (directly returning a value), or _asynchronous_ (passing a value to callbacks). If done properly, when you convert code into a ZIO effect, this code will be stored inside the effect so that it can be managed by ZIO, and benefit from features like retries, timeouts, and automatic error logging. The conversion functions that ZIO has allow you to seamlessly use all features of ZIO with non-ZIO code written in Scala or Java, including third-party libraries. ### Synchronous Code Synchronous code can be converted into a ZIO effect using `ZIO.attempt`: ```scala val readLine: ZIO[Any, Throwable, String] = ZIO.attempt(StdIn.readLine()) ``` The error type of the resulting effect will always be `Throwable`, because synchronous code may throw exceptions with any value of type `Throwable`. If you know for a fact that some code does not throw exceptions (except perhaps runtime exceptions), you can convert the code into a ZIO effect using `ZIO.succeed`: ```scala def printLine(line: String): UIO[Unit] = ZIO.succeed(println(line)) ``` Sometimes, you may know that code throws a specific exception type, and you may wish to reflect this in the error parameter of your ZIO effect. For this purpose, you can use the `ZIO#refineToOrDie` method: ```scala val readLine2: ZIO[Any, IOException, String] = ZIO.attempt(StdIn.readLine()).refineToOrDie[IOException] ``` ### Asynchronous Code Asynchronous code that exposes a callback-based API can be converted into a ZIO effect using `ZIO.async`: ```scala object legacy { def login( onSuccess: User => Unit, onFailure: AuthError => Unit): Unit = ??? } val login: ZIO[Any, AuthError, User] = ZIO.async[Any, AuthError, User] { callback => legacy.login( user => callback(ZIO.succeed(user)), err => callback(ZIO.fail(err)) ) } ``` Asynchronous effects are much easier to use than callback-based APIs, and they benefit from ZIO features like interruption, resource-safety, and error management. ## Blocking Synchronous Code Some synchronous code may engage in so-called _blocking IO_, which puts a thread into a waiting state, as it waits for some operating system call to complete. For maximum throughput, this code should not run on your application's primary thread pool, but rather, in a special thread pool that is dedicated to blocking operations. ZIO has a blocking thread pool built into the runtime, and lets you execute effects there with `ZIO.blocking`: ```scala def download(url: String) = ZIO.attempt { Source.fromURL(url)(Codec.UTF8).mkString } def safeDownload(url: String) = ZIO.blocking(download(url)) ``` As an alternative, if you wish to convert blocking code directly into a ZIO effect, you can use the `ZIO.attemptBlocking` method: ```scala val sleeping = ZIO.attemptBlocking(Thread.sleep(Long.MaxValue)) ``` The resulting effect will be executed on ZIO's blocking thread pool. If you have some synchronous code that will respond to Java's `Thread.interrupt` (such as `Thread.sleep` or lock-based code), then you can convert this code into an interruptible ZIO effect using the `ZIO.attemptBlockingInterrupt` method. Some synchronous code can only be cancelled by invoking some other code, which is responsible for canceling the running computation. To convert such code into a ZIO effect, you can use the `ZIO.attemptBlockingCancelable` method: ```scala def accept(l: ServerSocket) = ZIO.attemptBlockingCancelable(l.accept())(ZIO.succeed(l.close())) ``` ## Next Steps If you are comfortable creating effects from values, converting from Scala types into effects, and converting synchronous and asynchronous code into effects, the next step is learning [basic operations](basic-operations.md) on effects. --- ## Handling Errors ZIO effects may fail due to foreseen or unforeseen problems. In order to help you build robust applications, ZIO tracks foreseen errors at compile-time, letting you know which effects can fail, and how they can fail. For non-recoverable problems, ZIO gives you full insight into the cause of failures (even if unexpected or catastrophic), preserving all information and automatically logging unhandled errors. In this section, you will learn about some of the tools ZIO gives you to build applications with robust error management. ## Either With the `ZIO#either` method, you can transform an effect that fails into an infallible effect that places both failure and success into Scala's `Either` type. This brings the error from the error channel to the success channel, which is useful because many ZIO operators work on the success channel, not the error channel. ```scala val zeither: ZIO[Any, Nothing, Either[String, Nothing]] = ZIO.fail("Uh oh!").either ``` ## Catching All Errors If you want to catch and recover from all types of recoverable errors and effectfully attempt recovery, then you can use the `catchAll` method, which lets you specify an error handler that returns the effect to execute in the event of an error: ```scala val z: ZIO[Any, IOException, Array[Byte]] = openFile("primary.json").catchAll { error => for { _ <- ZIO.logErrorCause("Could not open primary file", Cause.fail(error)) file <- openFile("backup.json") } yield file } ``` In the error handler passed to `catchAll`, you may return an effect with a _different_ error type (perhaps `Nothing`, if the error handler cannot fail), which is then reflected in the type of effect returned by `catchAll`. ## Catching Some Errors If you want to catch and recover from only some types of recoverable errors and effectfully attempt recovery, then you can use the `catchSome` method: ```scala val data: ZIO[Any, IOException, Array[Byte]] = openFile("primary.data").catchSome { case _ : FileNotFoundException => openFile("backup.data") } ``` Unlike `catchAll`, `catchSome` cannot reduce or eliminate the error type, although it can widen the error type to a broader class of errors. ## Fallback You can try one effect or if it fails, try another effect with the `orElse` combinator: ```scala val primaryOrBackupData: ZIO[Any, IOException, Array[Byte]] = openFile("primary.data").orElse(openFile("backup.data")) ``` ## Folding In the Scala standard library, the data types `Option` and `Either` have a `fold` method, which lets you handle both failure and success cases at the same time. In a similar fashion, `ZIO` effects also have several methods that allow you to handle both failure and success at the same time. The first fold method, `fold`, lets you separately convert both failure and success into some common type: ```scala lazy val DefaultData: Array[Byte] = Array(0, 0) val primaryOrDefaultData: ZIO[Any, Nothing, Array[Byte]] = openFile("primary.data").fold( _ => DefaultData, // Failure case data => data) // Success case ``` The second fold method, `foldZIO`, lets you separately handle both failure and success by specifying effects that will be executed in each respective case: ```scala val primaryOrSecondaryData: ZIO[Any, IOException, Array[Byte]] = openFile("primary.data").foldZIO( _ => openFile("secondary.data"), // Error handler data => ZIO.succeed(data)) // Success handler ``` The `foldZIO` method is almost the most powerful error recovery method in ZIO, with only `foldCauseZIO` being more powerful. Most other operators, such as `either` or `orElse`, are implemented in terms of these powerful methods. In the following additional example, `foldZIO` is used to handle both the failure and the success of the `readUrls` method: ```scala val urls: ZIO[Any, Nothing, Content] = readUrls("urls.json").foldZIO( error => ZIO.succeed(Content.NoContent(error)), success => fetchContent(success) ) ``` ## Retrying In order to deal with transient errors, which are the norm when interacting with external cloud systems, ZIO provides very powerful retry mechanisms. One of these mechanisms is the `ZIO#retry` method, which takes a `Schedule`, and returns a new effect that will retry the original effect if it fails, according to the specified schedule: ```scala val retriedOpenFile: ZIO[Any, IOException, Array[Byte]] = openFile("primary.data") .retry(Schedule.recurs(5)) ``` The next most powerful function is `ZIO#retryOrElse`, which allows specification of a fallback to use if the effect does not succeed with the specified policy: ```scala val retryOpenFile: ZIO[Any, IOException, DefaultData) = openFile("primary.data") .retryOrElse(Schedule.recurs(5), (_, _) => ZIO.succeed(DefaultData)) ``` For more information on how to build schedules, see the documentation on [Schedule](../reference/schedule/index.md). ## Next Steps If you are comfortable with basic error handling, including applying simple retry logic to effects, the next step is to learn about safe [resource handling](handling-resources.md). --- ## Handling Resources Ensuring that your applications never leak resources is one of the keys to maximizing application throughput, minimizing latency, and maximizing per-node uptime. Yet, achieving resource safety in the presence of asynchronous operations, concurrency, and ZIO's interruption model (which will automatically cancel running effects anytime their results will no longer be used) is challenging. In this section, you will learn a few of the tools that ZIO provides to create safe applications that never leak resources, even in the case of failure, interruption, or defects in your application. ## Finalizing In many languages, the `try` / `finally` construct provides a language-level way to guarantee that when the `try` code exits, either normally or abnormally, the _finalizer_ code in the `finally` block will be executed. ZIO provides a version of this with the `ZIO#ensuring` method, whose guarantees hold across concurrent and async effects. ZIO goes one step further in automatically and losslessly aggregating errors from finalizers. As with `try` / `finally`, the `ensuring` method guarantees if the effect it is called on begins executing and terminates (either normally or abnormally), then the finalizer will begin execution. ```scala val finalizer: UIO[Unit] = ZIO.succeed(println("Finalizing!")) // finalizer: UIO[Unit] = Sync( // trace = "repl.MdocSession.MdocApp.finalizer(handling-resources.md:15)", // eval = // ) val finalized: IO[String, Unit] = ZIO.fail("Failed!").ensuring(finalizer) // finalized: IO[String, Unit] = DynamicNoBox( // trace = "repl.MdocSession.MdocApp.finalized(handling-resources.md:19)", // update = 1L, // f = zio.ZIO$$$Lambda$19563/0x00007f02a6ecf668@4761552 // ) ``` In ZIO, finalizers are not allowed to fail in any recoverable way, which means that you must handle all of the errors that your code can produce. Like `try` / `finally`, finalizers can be nested, and the failure of any inner finalizer will not affect outer finalizers. Nested finalizers will be executed in reverse order and sequentially, with later finalizers executed only after earlier finalizers. ## Acquire Release A common use for `try` is safely acquiring and releasing resources, such as new socket connections or opened files: ```scala val handle = openFile(name) try { processFile(handle) } finally closeFile(handle) ``` ZIO encapsulates this common pattern with `ZIO.acquireReleaseWith`, which allows you to specify an _acquire_ effect, which acquires a resource; a _release function_, which returns an effect to release the resource; and a _use function_, which returns an effect that _uses_ the resource. So long as the acquire effect succeeds, the release effect is guaranteed to be executed by the runtime system, even in the presence of errors or interruption. ```scala val groupedFileData: IO[IOException, Unit] = ZIO.acquireReleaseWith(openFile("data.json"))(closeFile(_)) { file => for { data <- decodeData(file) grouped <- groupData(data) } yield grouped } ``` Like `ensuring`, `acquireReleaseWith` has compositional semantics, so if one `acquireReleaseWith` is nested inside another `acquireReleaseWith`, and the outer resource is acquired, then the outer release will always be called, even if, for example, the inner release fails. For resources which implement the AutoClosable interface, the convenience method `fromAutoClosable` can be used, which can be seen as the ZIO equivalent of try-with-resource. ```scala val bytesInFile: IO[Throwable, Int] = ZIO.scoped { for { stream <- ZIO.fromAutoCloseable(openFileInputStream("data.json")) data <- ZIO.attemptBlockingIO(stream.readAllBytes()) } yield data.length } ``` ## Next Steps If you are comfortable with basic resource handling, the next step is to learn about [basic concurrency](basic-concurrency.md). --- ## Getting Started with ZIO ## Teach Your Coding Agent Latest ZIO Knowledge The `zio-knowledge` skill teaches your coding agent to fetch live documentation from zio.dev before answering any ZIO question — so you always get accurate, up-to-date answers, not guesses from stale training data: ```bash npx skills add zio/zio-skills --skill zio-knowledge ``` ## Installation Include ZIO in your project by adding the following to your `build.sbt` file: ``` libraryDependencies += "dev.zio" %% "zio" % "2.1.26" ``` If you want to use ZIO streams, you should also include the following dependency: ``` libraryDependencies += "dev.zio" %% "zio-streams" % "2.1.26" ``` ## Main Your application can extend `ZIOAppDefault`, which provides a complete runtime system and allows you to write your whole program using ZIO: ```scala object MyApp extends ZIOAppDefault { def run = myAppLogic val myAppLogic = for { _ <- printLine("Hello! What is your name?") name <- readLine _ <- printLine(s"Hello, ${name}, welcome to ZIO!") } yield () } ``` The `run` method should return a ZIO value which has all its errors handled, which, in ZIO parlance, is an unexceptional ZIO value. One way to do this is to invoke `fold` over a ZIO value, to get an unexceptional ZIO value. That requires two handler functions: `eh: E => B` (the error handler) and `ah: A => B` (the success handler). If `myAppLogic` fails, `eh` will be used to get from `e: E` to `b: B`; if it succeeds, `ah` will be used to get from `a: A` to `b: B`. `myAppLogic`, as folded above, produces an unexceptional ZIO value, with `B` being `Int`. If `myAppLogic` fails, there will be a 1; if it succeeds, there will be a 0. --- If you are integrating ZIO into an existing application, using dependency injection, or do not control your main function, then you can create a runtime system in order to execute your ZIO programs: ```scala object IntegrationExample { val runtime = Runtime.default Unsafe.unsafe { implicit unsafe => runtime.unsafe.run(ZIO.attempt(println("Hello World!"))).getOrThrowFiberFailure() } } ``` Ideally, your application should have a _single_ runtime, because each runtime has its own resources (including thread pool and unhandled error reporter). ## Console ZIO provides a module for interacting with the console. You can import the functions in this module with the following code snippet: If you need to print text to the console, you can use `print` and `printLine`: ```scala // Print without trailing line break Console.print("Hello World") // Print string and include trailing line break Console.printLine("Hello World") ``` If you need to read input from the console, you can use `readLine`: ```scala val echo = Console.readLine.flatMap(line => Console.printLine(line)) ``` --- ## Performance ZIO is a high-performance framework that is powered by non-blocking fibers (which will move to _virtual threads_ under Loom). ZIO's core execution engine minimizes allocations and automatically cancels all unused computation. All data structures included with ZIO are high-performance and non-blocking, and to the maximum extent possible on the JVM, non-boxing. The `benchmarks` project has a variety of benchmarks that compare the performance of ZIO with other similar projects in the Scala and Java ecosystems, demonstrating 2-100x faster performance in some cases. Benchmarks to compare the performance of HTTP, GraphQL, RDMBS, and other ZIO integrations can be found in those respective projects. --- ## Platforms ZIO provides a consistent interface across platforms to the maximum extent possible, allowing developers to write code once and deploy it everywhere. However, there are some unavoidable differences between platforms to be aware of. ## JVM ZIO supports Java versions 11 and above and Scala versions 2.12, 2.13, and 3.x. On the JVM, effects may be executed on a blocking thread pool using methods like `ZIO.blocking` and `ZIO.attemptBlocking`. See the documentation on [Creating Effects](creating-effects.md) for further discussion on blocking operations. ## Scala.js ZIO supports Scala.js 1.0. While ZIO is a zero dependency library, some basic capabilities of the platform are assumed. In particular, due to the absence of implementations for certain `java.time` methods in Scala.js, users must bring their own `java.time` dependency. The one used by ZIO in its own internal test suites is [scala-java-time](https://github.com/cquiroz/scala-java-time). It can be added as a dependency like so: ```scala libraryDependencies ++= Seq( "io.github.cquiroz" %%% "scala-java-time" % "2.2.0", "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.2.0" ) ``` Because of the single threaded execution model of Javascript, blocking operations are not supported on Scala.js. In addition, several other methods are not supported or are unsafe on Scala.js: * The `readLine` method in the `Console` service is not supported because reading a line from the console blocks until input is received and the underlying method from the Scala standard library is not implemented on Scala.js. * The synchronous execution methods on `Runtime` are not safe. All of these methods return a value synchronously and may require blocking if the effect includes asynchronous operations, including yield points introduced by the runtime to guarantee fairness. Users should use asynchronous execution methods instead. ## Scala Native Support for Scala Native is currently experimental. More details will be added regarding support for the Scala Native platform when they are available. --- ## Running Effects ZIO effects are precise plans that _describe_ a computation or interaction. Ultimately, every effect must be _executed_ by the ZIO runtime. In this section, you will learn about the several ways that ZIO provides for you to execute effects in your application. ## App If you construct a single effect for your whole program, the most natural way to run the effect is to extend `ZIOAppDefault`. This class provides Scala with a JVM-compatible main function, so it can be called from IDEs and launched from the command-line. All you have to do is implement the `run` method by returning the effect to run. ```scala object MyApp extends ZIOAppDefault { def run = for { _ <- printLine("Hello! What is your name?") name <- readLine _ <- printLine(s"Hello, ${name}, welcome to ZIO!") } yield () } ``` If you are using a custom environment for your application, you will have to supply your environment to the effect (using `ZIO#provideEnvironment` or, if you are using layers, `ZIO#provide`) before you return it from `run`. `ZIOAppDefault` does not know how to supply custom environments. ## Default Runtime Most applications are not greenfield, meaning they must integrate with legacy code and procedural libraries and frameworks. In these cases, a better solution for running effects is to create a `Runtime`, which can be passed around and used to run effects wherever required. ZIO contains a default runtime called `Runtime.default`. To access it, merely use ```scala val runtime = Runtime.default ``` Once you have a runtime, you can use it to execute effects: ```scala Unsafe.unsafe { implicit unsafe => runtime.unsafe.run(ZIO.attempt(println("Hello World!"))).getOrThrowFiberFailure() } ``` In addition to `run`, which is for synchronous execution, there are other methods available on `Runtime` that support asynchronous execution. ## Custom Runtime If you are using a custom environment for your application, you may find it useful to create a `Runtime` specifically tailored for that environment. A custom `Runtime[R]` can be created with a `ZEnvironment[R]` (which holds the context required in order to execute your effects), as well as fiber refs and runtime flags (which can generally be set to default values). For example, the following creates a `Runtime` that can provide an `Int` to effects : ```scala val myRuntime: Runtime[Int] = Runtime(ZEnvironment[Int](42), FiberRefs.empty, RuntimeFlags.default) ``` ## Error Reporting The ZIO runtime system automatically logs all errors encountered when executing your effects, so long as those errors are not handled by your ZIO code. You can specify a custom logger easily using _ZIO Logging_, which can intercept these logged errors and handle them as configured by your logging backend. ## Next Steps If you are comfortable with running effects, congratulations! You are now ready to dive into other sections on the ZIO website covering data types, use cases, and interop with other systems. Refer to the Scaladoc for detailed documentation on all the core ZIO types and methods. --- ## Summary ZIO is a next-generation framework for building cloud-native applications on the JVM. With a beginner-friendly yet powerful functional core, ZIO lets developers quickly build best-practice applications that are highly scalable, testable, robust, resilient, resource-safe, efficient, and observable. At the heart of ZIO is a powerful data type called `ZIO`, which is the fundamental building block for every ZIO application. ## ZIO The `ZIO` data type is called a _functional effect_, and represents a unit of computation inside a ZIO application. Similar to a blueprint or a workflow, functional effects are precise plans that _describe_ a computation or interaction. When executed by the ZIO runtime system, a functional effect will either fail with some type of error, or succeed with some type of value. Like the `List` data type, the `ZIO` data type is a _generic_ data type, and uses type parameters for improved type-safety. The `List` data type has a single type parameter, which represents the type of element that is stored in the `List`. The `ZIO` data type has three type parameters: `ZIO[R, E, A]`. The type parameters of the `ZIO` data type have the following meanings: - **`R` - Environment Type**. The environment type parameter represents the type of contextual data that is required by the effect before it can be executed. For example, some effects may require a connection to a database, while others might require an HTTP request, and still others might require a user session. If the environment type parameter is `Any`, then the effect has no requirements, meaning the effect can be executed without first providing it any specific context. - **`E` - Failure Type**. The failure type parameter represents the type of error that the effect can fail with when it is executed. Although `Exception` or `Throwable` are common failure types in ZIO applications, ZIO imposes no requirement on the error type, and it is sometimes useful to define custom business or domain error types for different parts of an application. If the error type parameter is `Nothing`, it means the effect cannot fail. - **`A` - Success Type**. The success type parameter represents the type of success that the effect can succeed with when it is executed. If the success type parameter is `Unit`, it means the effect produces no useful information (similar to a `void`-returning method), while if it is `Nothing`, it means the effect runs forever, unless it fails. As several examples of how to interpret the types of ZIO effects: - An effect of type `ZIO[Any, IOException, Byte]` has no requirements, and when executed, such an effect may fail with a value of type `IOException`, or may succeed with a value of type `Byte`. - An effect of type `ZIO[Connection, SQLException, ResultSet]` requires a `Connection`, and when executed, such an effect may fail with a value of type `SQLException`, or may succeed with a value of type `ResultSet`. - An effect of type `ZIO[HttpRequest, HttpFailure, HttpSuccess]` requires an `HttpRequest`, and when executed, such an effect may fail with a value of type `HttpFailure`, or may succeed with a value of type `HttpSuccess`. The environment type parameter is a _composite type parameter_, because sometimes, a single effect can require _multiple_ values of _different_ types. If you see that an effect has a type of `ZIO[UserSession with HttpRequest, E, A]` (Scala 2.x) or `ZIO[UserSession & HttpRequest, E, A]` (Scala 3.x), it means that the effect requires multiple contextual values before it can be executed. Although this analogy is not precise, a ZIO effect can be thought of as a function: ```scala R => Either[E, A] ``` This function requires an `R` and produces a failure of type `E` or a success value of type `A`. ZIO effects are not actually functions, of course, because they model complex computations and interactions, which may be asynchronous, concurrent, or resourceful. ## Type Aliases The `ZIO` data type is the only effect type in ZIO. However, there are a family of type aliases that reduce the need to type: - `UIO[A]` — A type alias for `ZIO[Any, Nothing, A]`, representing an effect that has no requirements, cannot fail, and can succeed with an `A`. - `URIO[R, A]` — A type alias for `ZIO[R, Nothing, A]`, representing an effect that requires an `R`, cannot fail, and can succeed with an `A`. - `Task[A]` — A type alias for `ZIO[Any, Throwable, A]`, representing an effect that has no requirements, may fail with a `Throwable` value, or succeed with an `A`. - `RIO[R, A]` — A type alias for `ZIO[R, Throwable, A]`, representing an effect that requires an `R`, may fail with a `Throwable` value, or succeed with an `A`. - `IO[E, A]` — A type alias for `ZIO[Any, E, A]`, representing an effect that has no requirements, may fail with an `E`, or succeed with an `A`. **Tips For Getting Started With Type Aliases** If you are new to functional effects, we recommend starting with the `Task` type, which has a single type parameter and corresponds most closely to the `Future` data types built into the Scala and Java standard libraries. If you are using _Cats Effect_ libraries, you may find the `RIO` type useful, since it allows you to thread context through third-party libraries. No matter what type alias you use in your application, `UIO` can be useful for describing infallible effects, including those resulting from handling all errors. Finally, if you are an experienced functional programmer, then direct use of the `ZIO` data type is recommended, although you may find it useful to create your own family of type aliases in different parts of your application. ## Next Steps If you are comfortable with the `ZIO` data type, and its family of type aliases, the next step is learning how to [create effects](creating-effects.md). --- ## Architectural Patterns In this section, we are going to talk about the design elements of a ZIO application and the ZIO idiomatic way of structuring codes to write ZIO applications. ## Onion Architecture Onion architecture is a software architecture pattern that is used to create loosely coupled, maintainable, and testable applications by layering the application into a set of concentric circles. - The innermost layer contains the domain model. Its language has the highest level of abstraction. - From the very center, the language domain model is surrounded successively by other layers, each of which is more technical and has a lower level of abstraction than the previous one. - The outermost layer contains the final language is the one that is closest to the environment in which the application is running. For example, the outermost layer could be the user interface, a web API, etc. Onion architecture is based on the _inversion of control_ principle. So each layer is dependent on the underlying layer, but not on the layers above it. This means that the innermost layer is independent of any other layers. In ZIO by taking advantage of both functional and object-oriented programming, we can implement onion architecture in a very simple and elegant way. To implement this architecture, please refer to the [Writing ZIO Services](../service-pattern/index.md) section which empowers you to create layers (services) in the onion architecture. In order to assemble all layers and make the whole application work, please refer to the [Dependency Injection In ZIO](../di/index.md) section. ## Streaming Architecture Many reasons make streaming architecture a good choice for building applications: - From the technical perspective when we are dealing with files, sockets, HTTP requests, databases, etc we are working with streams of data. - In addition from a business standpoint, the area of data processing is growing rapidly and the need for processing continuous streams of data is increasing, such as real-time analytics, fraud detection, monitoring, social media platforms, financial trading, etc. In such cases, we may decide to use streaming architecture. ZIO Streams is a library that provides a purely functional, composable, and type-safe way to work with streams of data. We can use ZIO Streams to model both stateful and stateless streaming data processing pipelines. ZIO Streams is on top of ZIO. So we can think of `ZStream` as a specialized functional effect that has more power than `ZIO`. It is built on top of ZIO and supports backpressure using a pull-based model. To learn more about ZIO Streams, please refer to the [ZIO Streams](../stream/index.md) section. ## Sidecar Pattern The sidecar pattern is a microservice architecture pattern that is used to separate cross-cutting concerns from the main business logic. It is a very useful pattern when we have to deal with concerns like logging, metrics, profiling, monitoring, etc. These concerns are not part of the main service logic, but they are important for the service to work correctly. In ZIO, we can implement the sidecar pattern by using compositional apps, or by using the `bootstrap` layer. ### Composable ZIO Applications In the following example, as we have multiple applications (`UserApp` and `DocumentApp`), we use compositional apps to implement this pattern: ```scala object UserApp extends ZIOAppDefault { def run = Server.serve(userHttpApp).provide(Server.defaultWithPort(8080)) } object DocumentApp extends ZIOAppDefault { def run = Server.serve(documentHttpApp).provide(Server.defaultWithPort(8081)) } object Metrics extends ZIOAppDefault { private val metricsConfig = ZLayer.succeed(MetricsConfig(5.seconds)) def run = Server .serve( Routes(Method.GET / "metrics" -> handler(ZIO.serviceWithZIO[PrometheusPublisher](_.get.map(Response.text))) ) ) .provide( Server.defaultWithPort(8082), metricsConfig, prometheus.publisherLayer, prometheus.prometheusLayer ) } object MainApp extends ZIOApp.Proxy(UserApp <> DocumentApp <> Metrics) ``` ### Bootstrap Layer If we had only one application, we could use the `bootstrap` layer to implement this pattern: ```scala object MetricsService { private val metricsConfig = ZLayer.succeed(MetricsConfig(5.seconds)) private val exporter: ZLayer[PrometheusPublisher, Nothing, Unit] = ZLayer.fromZIO { Server .serve( Routes(Method.GET / "metrics" -> handler(ZIO.serviceWithZIO[PrometheusPublisher](_.get.map(Response.text))) ) ) .provideSome[PrometheusPublisher](Server.defaultWithPort(8081)) .forkDaemon .unit } val layer: ZLayer[Any, Nothing, Unit] = ZLayer.make[Unit]( exporter, metricsConfig, prometheus.publisherLayer, prometheus.prometheusLayer ) } object UserAoo extends ZIOAppDefault { override val bootstrap = MetricsService.layer def run = Server.serve(userHttpApp).provideSome(Server.defaultWithPort(8080)) } ``` --- ## Functional Design Patterns When designing an API, there are patterns that are commonly used. In this section, we are going to talk about some of these patterns: 1. Functional Data Modeling 2. Functional Domain Modeling 1. Declarative Encoding 2. Executable Encoding ## Functional Data Modeling Before we start talking about functional data modeling, let's first recap the object-oriented way of modeling data. The essence of object-oriented data modeling is inheritance. We have classes, traits, abstract classes, and subtyping. The core idea is to describe the commonalities between different types of data in a base interface or abstract class, and then extend it to describe the differences in the subclasses; so each subclass has its type-specific details while sharing the commonalities with the base type: ```scala abstract class Event { def id: String def timestamp: Long } case class ClickEvent(id: String, timestamp: Long, element: String) extends Event case class ViewEvent(id: String, timestamp: Long, page: String) extends Event ``` There is some problem with this approach, let's iterate some of them: One of the problems with this approach is that it is not a good fit when we want to write generic operations on the `Event` type. For example, if we want to write an operation that changes the timestamp of an event, we should match the input event and then do the transformation for each case. Then finally, we should use `asInstanceOf` to cast the result back to the original type: ```scala def updateTimestamp[E <: Event](event: E, timestamp: Long): E = event match { case e: ClickEvent => e.copy(timestamp = timestamp).asInstanceOf[E] case e: ViewEvent => e.copy(timestamp = timestamp).asInstanceOf[E] } ``` This introduces a lot of boilerplate code. It also has a type-safety issue. If we forget to add all the cases for the match expression, the compiler will not be able to detect it. In functional data modeling, we don't use inheritance. All tools we have are sum types and product types. By using these two types, we can mathematically model the data. Let's try to model the previous example using sum and product types: ```scala case class Event(id: String, timestamp: Long, details: EventType) sealed trait EventType object EventType { final case class ClickEvent(element: String) extends EventType final case class ViewEvent(page: String) extends EventType } ``` In this approach, we describe commonalities with product types, and differences with sum types. So we push `id`, `timestamp`, and `details` to the `Event` case class, and encode the type-specific details of the event in the `EventType` which is a sum type. The product and sum types are called "Algebraic Data Types" (ADT). They are the building blocks of modeling data in functional programming: - **Product types** are the cartesian product of the types they contain. For example, `Event` is the product of `String`, `Long`, and `EventType`. In scala, we use `case class` to model product types. - **Sum types** are the disjoint union of the types they represent. For example, `EventType` is the either `ClickEvent` or `ViewEvent`. In scala 2, we use `sealed trait`s and In Scala 3, we use `enum`s to model sum types. ## Functional Domain Modeling Functional domain modeling is the process of modeling solutions to problems in a specific domain using functional programming. It is a very broad topic, and we are not going to cover all the details here. However, we are going to talk about the general patterns that are commonly used in functional domain modeling, in a nutshell. In functional programming, we have two primary tools: 1. Nouns (Data) 2. Verbs (Operators) So to provide a solution to every domain problem, we should follow these steps: 1. **Extracting the Core Model**— First, we need to focus on extracting the "minimum" information required to describe the "solution" to the "fundamental problem" in that domain. So we should ask ourselves, "what is the most fundamental problem we have in this domain?". 2. **Providing Operators**— Once we find out what is the core model of our domain, we should provide a set of "orthogonal operators" that are going to provide a "solution" to the "complex problems" by combining sub-problems. 3. **Packaging the Data Type**— To achieve great modularity, we package both the "core model" and "operators" in one place which is called "data type". 4. **Defining Constructors**— Also to have a better ergonomic API, we put all solutions to the "basic and simple problems" in that domain in the companion object of the "data type". The ZIO ecosystem defines all data types in such a way, including ZIO, Fiber, Reference, Stream, etc. There are two main encoding styles in functional domain modeling: 1. **Executable Encoding** is a functional domain modeling where we use operators and constructors to provide the "final" solution to the domain problems. 2. **Declarative Encoding** is a functional domain modeling where we use constructors and operators to provide the "description" of the solution to the domain problems. Later, we interpret the description to get the "final" solution. A real-life example will help you understand the differences between these two approaches. Let's say we want to write an effect system from scratch. To keep things simple we will create an effect system that only supports sequencial effects. So at the end, we would like to write a program like this: ```scala object Main extends scala.App { val app = for { _ <- IO.succeed(print("Enter your name: ")) name <- IO.succeed(scala.io.StdIn.readLine()) _ <- IO.succeed(println(s"Hello, $name")) } yield () app.unsafeRunSync() } ``` ### Executable Encoding The executable encoding is straightforward. After defining the core model, we just need to think about the execution steps for each operator. In the following example, we describe the core model as `IO` case class, which has a `thunk` of code that is going to be executed when we call `unsafeRunSync`: `case class IO[+A](private val thunk: () => A)`. We should provide a set of operators: `IO#map`, `IO#flatMap`, and `IO#unsafeRunSync`. We have also one constructor: `IO.succeed`. Implementing these operators requires us to think about how they should actually executed to produce the desired result. For example, when we implement `map`, should execute the `thunk` of the current `IO` and then apply the `f` function to the result. The same applies to `flatMap` and `unsafeRunSync`: ```scala final case class IO[+A](private val thunk: () => A) { def map[B](f: A => B): IO[B] = IO.succeed(f(thunk())) def flatMap[B](f: A => IO[B]): IO[B] = IO.succeed(f(thunk()).unsafeRunSync()) def unsafeRunSync(): A = thunk() } object IO { def succeed[A](value: => A): IO[A] = IO(() => value) } ``` Now we can run the greeting program with the above encoding. ### Declarative Encoding In contrast to the executable encoding, the declarative encoding is lazy. This means that the definition of the language is separate from how it is interpreted. In this example, we describe the problem of sequencing effects as a data type called `FlatMap` that contains two information: the first effect to be executed and the function that should be applied to the result of the first effect. To describe the thunk of code that will be executed by the interpreter, we create another data type called `Succeed`. Similarly, we need another data type called `SucceedNow` to describe the already evaluated values. ```scala sealed trait IO[+A] object IO { final case class SucceedNow[A](value: A) extends IO[A] final case class Succeed[A](thunk: () => A) extends IO[A] final case class FlatMap[A, B](io: IO[A], cont: A => IO[B]) extends IO[B] } ``` Assume we have written the following program: ```scala val app: IO[Int] = for { a <- IO.succeed(scala.io.StdIn.readLine("Enter the first number: ").toInt) b <- IO.succeed(scala.io.StdIn.readLine("Enter the second number: ").toInt) } yield (a + b) ``` This program will be translated into the following tree data structure using the declarative encoding: ```scala val app: IO[Int] = IO.FlatMap( IO.Succeed(() => scala.io.StdIn.readLine("Enter the first number: ").toInt), (a: Int) => IO.FlatMap( IO.Succeed(() => scala.io.StdIn.readLine("Enter the second number: ").toInt), (b: Int) => IO.SucceedNow(a + b) ) ) ``` The only operator that deals with the interpretation of the program is `unsafeRunSync`. Let's see how the whole encoding looks like: ```scala sealed trait IO[+A] { self => def map[B](f: A => B): IO[B] = flatMap(f andThen IO.succeedNow) def flatMap[B](f: A => IO[B]): IO[B] = IO.FlatMap(self, f) def unsafeRunSync(): A = { type Cont = Any => IO[Any] def run(stack: List[Cont], currentIO: IO[Any]): A = { def continue(value: Any) = stack match { case ::(cont, next) => run(next, cont(value)) case Nil => value.asInstanceOf[A] } currentIO match { case IO.SucceedNow(value) => continue(value) case IO.Succeed(thunk) => continue(thunk()) case IO.FlatMap(io, cont) => run(stack appended cont, io) } } run(stack = Nil, currentIO = self) } } object IO { def succeedNow[A](value: A): IO[A] = IO.SucceedNow(value) def succeed[A](value: => A): IO[A] = IO.Succeed(() => value) final case class SucceedNow[A](value: A) extends IO[A] final case class Succeed[A](thunk: () => A) extends IO[A] final case class FlatMap[A, B](io: IO[A], cont: A => IO[B]) extends IO[B] } ``` --- ## Non-functional Requirements ## Introduction Designing and architecting a software system is a complex task. We should consider both the functional and non-functional requirements of the system. The functional requirements are the features of the system which are directly related to the business domain and its problems. They are the core of the system and the main reason why we are designing and building the application. Non-functional requirements are characteristics of the system that are used to qualify it in terms of "what should the system be" rather than "what should the system do," e.g.: 1. [Correctness](#1-correctness) 2. [Testability](#2-testability) 3. [Maintainability](#3-maintainability) 4. [Low Latency](#4-low-latency) 5. [High Throughput](#5-high-throughput) 6. [Robustness](#6-robustness) 7. [Resiliency](#7-resiliency) 8. [Efficiency](#8-efficiency) 9. [Developer Productivity](#9-developer-productivity) In this article, from the perspective of application architecture, we are going to look at some design elements that we can apply to our ZIO applications to make them more ergonomic and maintainable. ## 1. Correctness Correctness is the ability of a system to do what it is supposed to do. ZIO provides us correctness property through local reasoning because of referential transparency and its type-safety. When we have referential transparency, we do not need to look at the whole program to understand the behavior of a piece of code. We can reason about the application behavior locally and then make sure that all components work together correctly, from button to top. The type system of ZIO also prevents us to introduce common bugs at runtime. Here are two examples: 1. **Resource Management**— When we have a ZIO effect that has a type of `ZIO[Scope, IOException, FileInputStream]`, we can be sure that this effect will open a resource, and we should care about closing it. So then by using `ZIO.scoped(effect)` we can be sure that the resource will be closed after the effect is executed and the type of effect will be changed to `ZIO[Any, IOException, FileInputStream`. To learn more about `ZIO.scoped` and resource management using `Scope`, please refer to the [Scope][11] of the [resource management][12]. 2. **Error Management**— In ZIO errors are typed, so we can describe all possible errors that can happen in our effect. And from the correctness perspective, the type system helps us to be sure we have handled all errors or not. For example, if we have an effect of type `ZIO[Any, IOException, FileInputStream]`, by looking at the effect type, we can be sure the effect is exceptional, and we should handle its error. To learn more about error management in ZIO, please refer to the [error management][13] section. ## 2. Testability ZIO has a strong focus on testability which supports: 1. Property-based Checking 2. Testing Effectful and Asynchronous Codes 3. Testing Passages of Time 4. Sharing Layers Between Specs 5. Resource Management While Testing 6. Dynamic Test Generation 7. Test Aspects (AOP) 8. Non-flaky Tests To learn more about testing in ZIO, please refer to the [testing][14] section. ## 3. Maintainability When we use ZIO, we take advantage of both functional and object-oriented programming paradigms to make our code maintainable: - By using functional programming we can make sure that our code is correct, readable, testable, and reusable. - The object-oriented programming paradigm helps us to make our code well-organized and highly cohesive by using objects, packages, and modules. The ZIO's support for type safety is another factor that makes our code maintainable, especially when we refactor our codes we can be sure that we are not breaking anything. ## 4. Low Latency Latency is the time it takes for a request to be processed and a response to be returned. ZIO is designed to support low latency applications by providing various concurrency and parallelism tools such as `ZIO.foreachPar`, `Fiber`, `Promise`, `Ref`, `Queue`, etc. To learn more about concurrency and parallelism in ZIO, please refer to the [concurrency][15] section. ## 5. High Throughput ZIO fibers are lightweight threads (green threads). They are very cheap to create and destroy. So we can potentially have thousands of fibers running in parallel on a single machine, which helps us to achieve high throughput: ```scala object MainApp extends ZIOAppDefault { def doWork(n: Int): ZIO[Any, Nothing, Unit] = ??? def run = ZIO .foreach(1 to 100000)(n => doWork(n).fork) .flatMap(f => Fiber.collectAll(f).join) } ``` Other than low-level concurrency tools like `Fiber`, `Promise`, `Ref`, etc., ZIO Streams is a high-level abstraction for processing high-throughput data streams: ```scala object MainApp extends ZIOAppDefault { def doWork(n: Int): ZIO[Any, Nothing, Unit] = ??? def run = ZStream .fromIterable(1 to 100000) .mapZIOParUnordered(Int.MaxValue)(doWork) .runDrain } ``` :::note The above examples are just for demonstration purposes. In real-world applications, depending on the nature of the problem to reach a better performance it may be better to control the level of parallelism instead of using unbounded parallelism. ::: Another factor that helps us to achieve high throughput is the fact that we may have high workloads for some periods. In such cases, we can benefit from buffering the incoming requests instead of rejecting them and trying to process them later. We can use [`Queue`][16] for this purpose or the [`ZStream#buffer` operator][17]. To learn more about ZIO Streams, please refer to the [ZIO Streams][18] section. ## 6. Robustness With the help of ZIO's error channel, we can write applications whose errors are fully specified and handled at the compile time. Having this feature helps us to make our applications more robust. It also gives us the ability to lossless translation of errors from one domain to another. For example, when writing a web application, we can reliably translate errors inside the application to HTTP response codes. ZIO uses the compile to ensure that we have mapped all possible errors to HTTP response codes. To learn more about error management in ZIO, please refer to the [error management][13] section. ## 7. Resiliency For resiliency, we can use ZIO's retry operator along with the retry policy to make our application resilient to failures. `Schedule` is a powerful composable data type that helps us to compose multiple policies together and make a complex retry policy: ```scala object MainApp extends ZIOAppDefault { sealed trait DownloadError extends Throwable case object BandwidthExceeded extends DownloadError case object NetworkError extends DownloadError // flaky api def download(id: String): ZIO[Any, DownloadError, Array[Byte]] = ??? def isRecoverable(e: DownloadError): Boolean = e match { case BandwidthExceeded => false case NetworkError => true } val policy = (Schedule.recurs(20) && Schedule.exponential(100.millis)) .whileInput(isRecoverable) def run = download("123").retry(policy) } ``` To learn more about resiliency and scheduling in ZIO, please refer to the [resiliency][19] section. ## 8. Efficiency ZIO is designed to be extraordinarily efficient. Let's take a look at some of the features that make ZIO efficient: 1. ZIO Streams are pull-based, so the source of the stream starts producing elements only when the stream is consumed. This lazy semantic helps us to avoid unnecessary work and save resources: ```scala def downloadAsCsv(id: String): ZStream[Db, IOException, Byte] = jdbc .selectMany(sql"SELECT * FROM events WHERE userId = $id") .map(toCSV) .via(ZPipeline.utf8Encode) .via(ZPipeline.gzip) ``` In the above example, tries to consume a minimum amount of computation that is necessary. So if we use this workflow in a web application, when the client downloads half of the CSV file, only half of the data will be pulled from the database. So we can save resources and infrastructure costs. 2. ZIO is designed to be interruptible (unlike the `Future` in Scala). So we can cancel any running effect at any time. This feature enables us to have efficient high-level operators such as `ZIO#race` on top of the ZIO interruption model. With `race` we can run two different workflows in parallel and the loser of the workflow will be canceled: ```scala val loaded = loadFromCache(productId).race(loadFromDb(productId)) ``` Or if we do a bunch of things in parallel and one of those things fails, all the other ones which are currently running in parallel will be canceled automatically: ```scala val aggregated = ZIO.foreach(account.statements) { statement => downloadStatement(statement.s3Bucket) }.map(aggregateStatements(_)) ``` If we timeout a workflow in ZIO, once the timeout is reached, the workflow will be canceled automatically: ```scala val timedOut = aggregated.timeout(10.seconds) ``` So in the above example, all running workflows will be simultaneously canceled once the timeout is reached and all resources will be released. 3. Another ZIO feature that helps us to have efficient workflows is its resource management. ZIO provides a great model for resource management with the help of the `Scope` data type. `Scope` is a contextual data type that whenever appears in the environment of an effect, denotes this effect will open one or more resources. Using `ZIO.scoped` we can ensure that all resources enclosed in this operator will be automatically released once the effect is completed or interrupted: ```scala def source(name: String): ZIO[Scope, Throwable, BufferedSource] = ZIO.acquireRelease(ZIO.attemptBlocking(scala.io.Source.fromFile(name)))(s => ZIO.succeedBlocking(s.close())) val fileContent: ZIO[Any, Throwable, String] = ZIO.scoped { source("file.txt").map(_.getLines()).map(_.mkString("\n")) } ``` In the above example, if we use the `fileContent` effect, we can be sure that the file handler will be released regardless of whether the effect is completed or interrupted. To learn more about resource management in ZIO, please refer to the [resource management][11] section. ## 9. Developer Productivity Developer experience and productivity are very important for choosing a technology for any large-scala and long-running project. Let's take a look at some features that make ZIO a great fit for developer productivity: 1. Referential Transparency and Purity 2. Composable Data Types 3. Type-safety and Compile time Error Checking 4. Easy to Refactor 5. Discoverability 1. Dot completion when developing with IDEs 2. Consistent naming conventions 6. Concise and Expressive API with Minimal Boilerplate 7. Expressive Compiler Errors 8. Empowering Meta-programming and Macros 9. [Maintainability](#3-maintainability) 10. Observability - [Logging][1] - [Tracing][2] - [Metrics][3] 11. [Debugging Facilities][4] 12. [Compile-time Execution Tracing][5] 13. [Automatic Dependency Graph Generation][6] 14. [Testability][7] 15. [Programming Without Type Classes][8] 16. Rich Ecosystem - Massive Amount of Libraries and Tools on JVM - [ZIO Official libraries][9] - [ZIO community libraries][10] [1]: ../../zio-logging/index.md [2]: ../../zio-telemetry/index.md [3]: ../observability/metrics/index.md [4]: ../../guides/migrate/migration-guide.md#debugging [5]: ../../guides/migrate/migration-guide.md#compile-time-execution-tracing [6]: ../di/automatic-layer-construction.md [7]: ../test/index.md [8]: https://www.youtube.com/watch?v=QDleESXlZJw [9]: ../../ecosystem/officials/index.md [10]: ../../ecosystem/community/index.md [11]: ../resource/scope.md [12]: ../resource/index.md [13]: ../error-management/index.md [14]: ../test/index.md [15]: ../concurrency/index.md [16]: ../concurrency/queue.md [17]: ../stream/zstream/operations.md#buffering [18]: ../stream/index.md [19]: ../schedule/index.md --- ## Programming Paradigms in ZIO It is important to realize that the programming paradigm used to write a software system has a significant impact on its design and architecture. In this section, we are going to talk the foundation of ZIO from the programming paradigm perspective: - Functional and object-oriented programming - Imperative and Declarative Programming - Structured Programming - Aspect-oriented Programming ## Functional and Object-Oriented Programming Every computer program is written in the form of a set of operations and data structures: - A data are nouns in the program, such as `Person`, `Address`, `Order`, etc. They represent a piece of information, configuration, or state that is used by operations. - Operations are verbs, such as `createOrder`, `updateOrder`, `deleteOrder`, and etc. They are methods or functions that operate on data. The way we organize these two elements in our program determines the programming paradigm we use; object-oriented programming (OOP) or functional programming (FP). ### Object-Oriented Programming In object-oriented programming, we organize our program by bundling related data and operations into a single unit called an object. Each object has its own state and behavior. This is the fundamental construct of object-oriented programming. All other constructs like classes, interfaces, inheritances, subtyping are built around this concept. Therefore, the object is the only option we have in object-oriented programming to organize our programs. We have only one hammer for all classes of design problems. So, we can say that the most important result of object-oriented programming is modularity. We can package related data and operations into a single unit and reuse them in other parts of our program. #### Classes In statical-typed object-oriented programming, we can define a "category" of objects by using a class. A class is a blueprint that defines the structure of all objects in that category. A class is basically define the whole class of objects that all have similarities. We can create an instance of a class by using the `new` keyword. This instance is called an object. #### Subtyping Another great feature of object-oriented programming is sub-typing. We can define a new class that inherits from an existing class. This new class is called a sub-class or a child class, and the existing class is called a super-class or a parent class. Using sub-typing, we can define a whole class of objects and then classify it into sub-classes. #### Interfaces and Polymorphism In object-oriented programming, we can also define an interface. An interface is a contract that defines the behavior that is shared by all classes that implement that interface. Using interfaces, we can achieve polymorphism when writing services. For example, we can define a `Logger` interface that defines the `log` method. Then we can define a `ConsoleLogger`, `FileLogger`, or `JsonLogger` that all implement the same `Logger` interface. This way, we can use the same `Logger` interface to inject different implementations of the `Logger` interface into our services. ### Functional Programming In the previous section, we discussed object-oriented programming and saw that the object is the basis of object-oriented programming. Let's talk about functional programming now and see what its basis is. A functional program is modeled as a set of mathematical functions. By mathematical functions, we mean those that take an immutable input and produce an immutable output while having referential transparency. Functions are the basis of FP, and the basis of functions is the lambda. Lambdas are functions that can be passed as arguments to other functions or returned as results. So we can say that the lambda is a fancy term for first-class functions (functions as values). In contrast to object-oriented programming, functional programming separates data and operations into two different worlds. Data is immutable, and operations are pure functions. This separation of data and operations is the fundamental philosophy of functional programming. In FP, we have only two building blocks to model our programs: - Data (Algebraic data types) - Operations (Functions) We describe our data (input and output) using constructs called "algebraic data types" which for Scala programmers means _sealed traits_ (or enums) and ـcase classesـ. So there is two building blocks for describing data in FP. To describe operations, we have functions. Separation of data and operations is the fundamental concept of functional programming. In contrast to object-oriented programming, we have no tools for abstraction, modularity, interfaces, and polymorphism. ### Embracing Both Functional and Object-Oriented Programming ZIO is a functional programming library which also brings the power of object-oriented programming into the functional world. It tries to combine the best of both worlds. We use FP to achieve **code maintainability** and OOP to achieve **code organization**: 1. Functional programming gives us the following tools in terms of **code maintainability**: - **Data Modeling** using Algebraic Data Types - **Functional Design** using functions to create Domain Specific Languages (DSLs) - **Composability** using Pure and Referentially Transparent Functions 2. Object-oriented programming gives us the following tools in terms of **code organization**: - **Methods** which help us to operate on a specific data attached to an object - **Constructors** which help us to create a new instance of a data type - **Modules** which allows us to bundle together related operations into a single unit So, we leverage the power of both FP and OOP to build a better software system in ZIO. ## Imperative and Declarative Programming Another important aspect of programming paradigms is the difference between imperative and declarative programming. In imperative programming, we describe the steps ("How") the computer should take to solve a problem. In declarative programming, we describe the problem itself ("What") and let the computer figure out the steps to solve it. Although ZIO has a declarative API in terms of a functional effect model —the ZIO runtime interprets the program as a set of effects and decides what steps to take to execute it—, it is imperative in comparison to other libraries like [Libretto](https://github.com/TomasMikula/libretto). ## Structured Programming The idea of structured concurrency is based on the structured programming paradigm. So, let's talk about structured programming first. In the early days of programming, we used to write programs in a linear manner. Program were composed of a series of instructions that executed one by one. Using goto statements, we could jump to any part of the program and change its execution flow. This was the first programming paradigm called procedural programming. Writing programs in such a linear fashion was error-prone and hard to maintain. Such a program was also complicated to read, understand, and reason about. Structured programming paradigms were introduced to solve this problem. Structured programming uses control structures like "if-then-else" to make the program flow more logical. Without these control structures, we cannot jump to any part of the program. In structured programming, we use control structures to organize our code into blocks. These blocks are called "structured blocks" and are the building blocks of structured programming. A structured control flow makes nested blocks of code with clear boundaries. Each new block of code has its own scope where all objects defined in that block are only visible inside that block. As a result, objects are bound to their enclosing blocks for their lifetime. Having clear scopes and lifetimes of objects make it easier to understand the control flow of the program. ZIO embraces the structured programming into the next level by using this paradigm in other areas of programming such as [structured concurrency](../fiber/index.md#structured-concurrency), [scope based resource management](../resource/scope.md), and also regional interruption model. ## Aspect Oriented Programming Aspect Oriented Programming (AOP) is a programming paradigm that allows us to separate cross-cutting concerns from the main program logic. Cross-cutting concerns are those that are not directly related to the main program logic but are still important to the program. Examples of cross-cutting concerns are logging, tracing, metrics, and security. ZIO embraces AOP by providing `ZIOAspect` in the "core" and `TestAspect` in the "test" module. Using these two data types, we can write aspects that can be applied to any ZIO effect or test. --- ## Hub A `Hub` is an asynchronous message hub. Publishers can publish messages to the hub and subscribers can subscribe to receive messagesfrom the hub. Unlike a `Queue`, where each value offered to the queue can be taken by _one_ taker, each value published to a hub can be received by _all_ subscribers. Whereas a `Queue` represents the optimal solution to the problem of how to _distribute_ values, a `Hub` represents the optimal solution to the problem of how to _broadcast_ them. The fundamental operators on a `Hub` are `publish` and `subscribe`: ```scala trait Hub[A] { def publish(a: A): UIO[Boolean] def subscribe: ZIO[Scope, Nothing, Dequeue[A]] } ``` The `publish` operator returns a `ZIO` effect that publishes a message of type `A` to the hub and succeeds with a value describing whether the message was successfully published to the hub. The `subscribe` operator returns a scoped `ZIO` effect that subscribes to the hub and unsubscribes from the hub when the scope is closed. Within the scope we have access to a `Dequeue`, which is a `Queue` that can only be dequeued from, that allows us to take messages published to the hub. For example, we can use a hub to broadcast a message to multiple subscribers like this: ```scala Hub.bounded[String](2).flatMap { hub => ZIO.scoped { hub.subscribe.zip(hub.subscribe).flatMap { case (left, right) => for { _ <- hub.publish("Hello from a hub!") _ <- left.take.flatMap(Console.printLine(_)) _ <- right.take.flatMap(Console.printLine(_)) } yield () } } } ``` A subscriber will only receive messages that are published to the hub while it is subscribed. So if we want to make sure that a particular message is received by a subscriber we must take care that the subscription has completed before publishing the message to the hub. We can do this by publishing a message to the hub within the scope of the subscription as in the example above or by using other coordination mechanisms such as completing a `Promise` when scope has been opened. Of course, in many cases such as subscribing to receive real time data we may not care about this because we are happy to just pick up with the most recent messages after we have subscribed. But for testing and simple applications this can be an important point to keep in mind. ## Constructing Hubs The most common way to create a hub is with the `bounded` constructor, which returns an effect that creates a new hub with the specified requested capacity. ```scala def bounded[A](requestedCapacity: Int): UIO[Hub[A]] = ??? ``` For maximum efficiency you should create hubs with capacities that are powers of two. Just like a bounded queue, a bounded hub applies back pressure to publishers when it is at capacity, so publishers will semantically block on calls to `publish` if the hub is full. The advantage of the back pressure strategy is that it guarantees that all subscribers will receive all messages published to the hub while they are subscribed. However, it does create the risk that a slow subscriber will slow down the rate at which messages are published and received by other subscribers. If you do not want this you can create a hub with the `dropping` constructor. ```scala def dropping[A](requestedCapacity: Int): UIO[Hub[A]] = ??? ``` A dropping hub will simply drop values published to it if the hub is at capacity, returning `false` on calls to `publish` if the hub is full to signal that the value was not successfully published. The advantage of the dropping strategy is that publishers can continue to publish new values so when there is space in the hub the newest values can be published to the hub. However, subscribers are no longer guaranteed to receive all values published to the hub and a slow subscriber can still prevent messages from being published to the hub and received by other subscribers. You can also create a hub with the `sliding` constructor. ```scala def sliding[A](requestedCapacity: Int): UIO[Hub[A]] = ??? ``` A sliding hub will drop the oldest value if a new value is published to it and the hub is at capacity, so publishing will always succeed immediately. The advantage of the sliding strategy is that a slow subscriber cannot slow down that rate at which messages are published to the hub or received by other subscribers. However, it creates the risk that slow subscribers may not receive all messages published to the hub. Finally, you can create a hub with the `unbounded` constructor. ```scala def unbounded[A]: UIO[Hub[A]] = ??? ``` An unbounded hub is never at capacity so publishing to an unbounded hub always immediately succeeds. The advantage of an unbounded hub is that it combines the guarantees that all subscribers will receive all messages published to the hub and that a slow subscriber will not slow down the rate at which messages are published and received by other subscribers. However, it does this at the cost of potentially growing without bound if messages are published to the hub more quickly than they are taken by the slowest subscriber. In general you should prefer bounded, dropping, or sliding hubs for this reason. However, unbounded hubs can be useful in certain situations where you do not know exactly how many values will be published to the hub but are confident that it will not exceed a reasonable size or want to handle that concern at a higher level of your application. ## Operators On Hubs In addition to `publish` and `subscribe`, many of the same operators that are available on queues are available on hubs. We can publish multiple values to the hub using the `publishAll` operator. ```scala trait Hub[A] { def publishAll(as: Iterable[A]): UIO[Boolean] } ``` We can check the capacity of the hub as well as the number of messages currently in the hub using the `size` and `capacity` operators. ```scala trait Hub[A] { def capacity: Int def size: UIO[Int] } ``` Note that `capacity` returns an `Int` because the capacity is set at hub creation and never changes. In contrast, `size` returns a `ZIO` effect that determines the current size of the hub since the number of messages in the hub can change over time. We can also shut down the hub, check whether it has been shut down, or await its shut down. Shutting down a hub will shut down all the queues associated with subscriptions to the hub, properly propagating the shut down signal. ```scala trait Hub[A] { def awaitShutdown: UIO[Unit] def isShutdown: UIO[Boolean] def shutdown: UIO[Unit] } ``` As you can see, the operators on `Hub` are identical to the ones on `Queue` with the exception of `publish` and `subscribe` replacing `offer` and `take`. So if you know how to use a `Queue` you already know how to use a `Hub`. In fact, a `Hub` can be viewed as a `Queue` that can only be written to. ```scala trait Hub[A] extends Enqueue[A] ``` Here the `Enqueue` type represents a queue that can only be enqueued. Enqueing to the queue publishes a value to the hub, shutting down the queue shuts down the hub, and so on. This can be extremely useful because it allows us to use a `Hub` anywhere we are currently using a `Queue` that we only write to. For example, say we are using the `into` operator on `ZStream` to send all elements of a stream of financial transactions to a `Queue` for processing by a downstream consumer. ```scala trait ZStream[-R, +E, +O] { def into( queue: Enqueue[Take[E, O]] ): ZIO[R, E, Unit] } ``` We would now like to have multiple downstream consumers process each of these transactions, for example to persist them and log them in addition to applying our business logic to them. With `Hub` this is easy because we can just use the `toQueue` operator to view any `Hub` as a `Queue` that can only be written to. ```scala type Transaction = ??? val transactionStream: ZStream[Any, Nothing, Transaction] = ??? val hub: Hub[Take[Nothing, Transaction]] = ??? transactionStream.into(hub) ``` All of the elements from the transaction stream will now be published to the hub. We can now have multiple downstream consumers process elements from the financial transactions stream with the guarantee that all downstream consumers will see all transactions in the stream, changing the topology of our data flow from one-to-one to one-to-many with a single line change. ## Hubs And Streams Hubs play extremely well with streams. We can create a `ZStream` from a subscription to a hub using the `fromHub` operator. ```scala object ZStream { def fromHub[O](hub: Hub[O]): ZStream[Any, Nothing, O] = ??? } ``` This will return a stream that subscribes to receive values from a hub and then emits every value published to the hub while the subscription is active. When the stream ends the subscriber will automatically be unsubscribed from the hub. There is also a `fromHubScoped` operator that returns the stream in the context of a scoped effect. ```scala object ZStream { def fromHubScoped[O]( hub: Hub[O] ): ZIO[Scope, Nothing, ZStream[Any, Nothing, O]] = ??? } ``` The scoped effect here describes subscribing to receive messages from the hub while the stream describes taking messages from the hub. This can be useful when we need to ensure that a consumer has subscribed before a producer begins publishing values. Here is an example of using it: ```scala for { promise <- Promise.make[Nothing, Unit] hub <- Hub.bounded[String](2) scoped = ZStream.fromHubScoped(hub).tap(_ => promise.succeed(())) stream = ZStream.unwrapScoped(scoped) fiber <- stream.take(2).runCollect.fork _ <- promise.await _ <- hub.publish("Hello") _ <- hub.publish("World") _ <- fiber.join } yield () ``` Notice that in this case we used a `Promise` to ensure that the subscription had completed before publishing to the hub. The scoped `ZIO` in the return type of `fromHubScoped` made it easy for us to signal when the subscription had occurred by using `tap` and completing the `Promise`. Of course in many real applications we don't need this kind of sequencing and just want to subscribe to receive new messages. In this case we can use the `fromHub` operator to return a `ZStream` that will automatically handle subscribing and unsubscribing for us. There is also a `fromHubWithShutdown` variant that shuts down the hub itself when the stream ends. This is useful when the stream represents your main application logic and you want to shut down other subscriptions to the hub when the stream ends. Each of these constructors also has `Chunk` variants, `fromChunkHub` and `fromChunkHubWithShutdown`, that allow you to preserve the chunked structure of data when working with hubs and streams. In addition to being able to create streams from subscriptions to hubs, there are a variety of ways to send values emitted by streams to hubs to build more complex data flow graphs. The simplest of these is the `toHub` operator, which constructs a new hub and publishes each element emitted by the stream to that hub. ```scala trait ZStream[-R, +E, +O] { def toHub[E1 >: E, O1 >: O]( capacity: Int ): ZIO[R with Scope, Nothing, Hub[Take[E1, O1]]] } ``` The hub will be constructed with the `bounded` constructor using the specified capacity. If you want to send values emitted by a stream to an existing hub or a hub created using one of the other hub constructors you can use the `runIntoHub` operator. ```scala trait ZStream[-R, +E, +O] { def runIntoHub[E1 >: E, O1 >: O]( hub: => Hub[Take[E1, O1]] ): ZIO[R, E1, Unit] } ``` There is an `runIntoHubScoped` variant of this if you want to send values to the hub in the context of a `Scope`. Here is the example above adapted to publish values from a stream to the hub: ```scala for { promise <- Promise.make[Nothing, Unit] hub <- Hub.bounded[Take[Nothing, String]](2) scoped = ZStream.fromHubScoped(hub).tap(_ => promise.succeed(())) stream = ZStream.unwrapScoped(scoped).flattenTake fiber <- stream.take(2).runCollect.fork _ <- promise.await _ <- ZStream("Hello", "World").runIntoHub(hub) _ <- fiber.join } yield () ``` Notice that we created a `Hub` of `Take` values this time. `Take` is an algebraic data type that represents the different potential results of pulling from a stream, including the stream emitting a chunk of values, failing with an error, or being done. Here we automatically unwrapped the `Take` values using the `flattenTake` operator on `ZStream`. In other cases where the subscriber was not a `ZStream` the `Take` value would allow the subscriber to observe whether the stream had emitted a value, failed with an error, or ended, and handle it appropriately. You can also create a sink that sends values to a hub. ```scala object ZSink { def fromHub[I]( hub: Hub[I] ): ZSink[Any, Nothing, I, Nothing, Unit] = ??? } ``` The sink will publish each value sent to the sink to the specified hub. Again there is a `fromHubWithShutdown` variant that will shut down the hub when the stream ends. Finally, `Hub` is used internally to provide a highly efficient implementation of the `broadcast` family of operators, including `broadcast` and `broadcastDynamic`. ```scala trait ZStream[-R, +E, +O] { def broadcast( n: Int, maximumLag: Int ): ZIO[R with Scope, Nothing, List[ZStream[Any, E, O]]] def broadcastDynamic( maximumLag: Int ): ZIO[R with Scope, Nothing, ZIO[Scope, Nothing, ZStream[Any, E, O]]] } ``` The `broadcast` operator generates the specified number of new streams and broadcasts each value from the original stream to each of the new streams. The `broadcastDynamic` operator returns a new `ZIO` value that you can use to dynamically subscribe and unsubscribe to receive values broadcast from the original stream. You don't have to do anything with `Hub` to take advantage of these operators other than enjoy their optimized implementation in terms of `Hub`. With `broadcast` and other `ZStream` operators that model distributing values to different streams and combining values from different streams it is straightforward to build complex data flow graphs, all while being as performant as possible. --- ## Introduction to Concurrent Programming in ZIO ## Overview Most of the time in concurrent programming we have a single state that we need to read and update concurrently. When we have multiple fibers reading or writing to the same memory location we encounter the race condition. The main goal in every concurrent program is to have a consistent view of states among all threads. There are two major concurrency models which try to solve this problem: 1. **Shared State** — In this model, all threads communicate with each other by sharing the same memory location. 2. **Message Passing (Distributed State)** — This model provides primitives for sending and receiving messages, and the state is distributed. Each thread of execution has its own state. The _Shared State_ model has two main solutions: 1. **Lock-based** — In the locking model, the general primitives for synchronization are _locks_ that control access to critical sections. When a thread wants to modify the critical section, it acquires the lock and says _I'm the only thread that is allowed to modify the state right now_, and after its work finished it unlocks the critical section and says _I'm done, any other thread can modify this memory section_. 2. **Non-blocking** — Non-blocking algorithms usually use hardware-intrinsic atomic operations like `compare-and-swap` (CAS), without using any locks. This method follows an optimistic design with a transactional memory mechanism to roll back in conflict situations. ## Implications of Locking Mechanism There are several drawbacks with lock-based concurrency: 1. Incorrect use of locks can lead to deadlocks. We need to care about the locking orders. If we don't place the locks in the right order, we may encounter a deadlock situation. 2. Identifying the critical section of code that is vulnerable to race conditions is overwhelming. We should always care about them and remember to lock everywhere it's required. 3. It makes our software design very sophisticated to become scalable and reliable. It doesn't scale with program size and complexity. 4. To prevent missing the releasing of the acquired locks, we should always care about exceptions and error handling inside locking sections. 5. The locking mechanism violates the encapsulation property of the pieces of our programs. So systems that are built on a locking mechanism are difficult to compose without knowing about their internals. ## Lock-free Concurrency Model As the lock-oriented programming does not compose and has lots of drawbacks, ZIO uses a _lock-free concurrency model_ which is a variation of non-blocking algorithms. The magic behind all of ZIO concurrency primitives is that they use the CAS (_compare-and-set_) operation. Let's see how the `modify` function of `Ref` is implemented without any locking mechanism: ```scala case class Ref[A](value: AtomicReference[A]) { self => def modify[B](f: A => (B, A)): UIO[B] = ZIO.succeed { var loop = true var b: B = null.asInstanceOf[B] while (loop) { val current = value.get val tuple = f(current) b = tuple._1 loop = !value.compareAndSet(current, tuple._2) } b } } ``` The idea behind the `modify` is that a variable is only updated if it still has the same value as the time we had read the value from the original memory location. If the value has changed, it retries in the while loop until it succeeds. ## Advantage of Using ZIO Concurrency Let's point out the key properties of the ZIO concurrency model: 1. **Composable** — Due to the use of the lock-free concurrency model, ZIO brings us composable concurrency primitives and lots of great combinators in a declarative style. :::note `Ref` and `Promise` and subsequently all other ZIO concurrency primitives that are on top of these two basic primitives **are not _transactionally_ composable**. We cannot do transactional changes across two or more of such concurrency primitives. They are susceptible to race conditions and deadlocks. **So don't use them if you need to perform an atomic operation on top of a composed sequence of multiple state-changing operations. In such a case use [`STM`](../stm/index.md) instead**. ::: 2. **Non-blocking** — All of the ZIO primitives are a hundred percent asynchronous and nonblocking. 3. **Resource Safety** — ZIO concurrency model comes with strong guarantees of resource safety. If any interruption occurs in between concurrent operations, it won't leak any resource. So it allows us to write compositional operators like timeout and racing without worrying about any leaks. ## Concurrency Primitives Let's take a quick look at ZIO concurrency primitives, what they are and why they exist. ### Basic Operations `Ref` and `Promise` are the two simple concurrency primitives which provide an orthogonal basis for building concurrency structures. They are assembly language of other concurrent data structures: - **[Ref](ref.md)** — `Ref` and all its variant like [`Ref.Synchronized`](refsynchronized.md) are building blocks for writing concurrent stateful applications. Anytime we need to share information between multiple fibers, and those fibers have to update the same information, they need to communicate through something that provides the guarantee of atomicity. So all of these `Ref` primitives are atomic and thread-safe. They provide us a reliable foundation for synchronizing concurrent programs. - **[Promise](promise.md)** — A `Promise` is a model of a variable that may be set a single time, and awaited on by many fibers. This primitive is very useful when we need some point of synchronization between two or multiple fibers. By using these two simple primitives, we can build lots of other asynchronous concurrent data structures like `Semaphore`, `Queue` and `Hub`. ### Others - **[Semaphore](semaphore.md)** — A `Semaphore` is an asynchronous (non-blocking) semaphore that plays well with ZIO's interruption. `Semaphore` is a generalization of a mutex. It has a certain number of permits, which can be held and released concurrently by different parties. Attempts to acquire more permits than available result in the acquiring fiber being suspended until the specified number of permits become available. - **[Queue](queue.md)** — A `Queue` is an asynchronous queue that never blocks, which is safe for multiple concurrent producers and consumers. - **[Hub](hub.md)** - A `Hub` is an asynchronous message hub that allows publishers to efficiently broadcast values to many subscribers. --- ## Promise A `Promise[E, A]` is a variable of `IO[E, A]` type that can be set exactly once. Promise is a **purely functional synchronization primitive** which represents a single value that may not yet be available. When we create a Promise, it always started with an empty value, then it can be completed exactly once at some point, and then it will never become empty or modified again. Promise is a synchronization primitive. So, it is useful whenever we want to wait for something to happen. Whenever we need to synchronize a fiber with another fiber, we can use Promise. It allows us to have fibers waiting for other fibers to do things. Any time we want to hand over work from one fiber to another fiber or anytime we want to suspend a fiber until some other fiber does a certain amount of work, we may want to use a Promise. Also, we can use `Promise` with `Ref` to build other concurrent primitives, like Queue and Semaphore. By calling `await` on the Promise, the current fiber blocks until that event happens. As we know, blocking fibers in ZIO don't actually block kernel threads. They are just blocking semantically, so when a fiber is blocked the underlying thread is free to run all other fibers. Promise is the equivalent of Scala's Promise. It's almost the same, except it has two type parameters, instead of one. Also instead of calling `future`, we need to call `await` on ZIO Promise to wait for the Promise to be completed. Promises can be failed with a value of type `E` or succeeded with a value of type `A`. So there are two ways we can complete a Promise, with failure or success, and whoever is waiting on the Promise will get back that failure or success. ## Operations ### Creation Promises can be created using `Promise.make[E, A]`, which returns `UIO[Promise[E, A]]`. This is a description of creating a promise, but not the actual promise. Promises cannot be created outside of IO, because creating them involves allocating mutable memory, which is an effect and must be safely captured in IO. ### Completing You can complete a `Promise[E, A]` in different ways: * successfully with a value of type `A` using `succeed` * with `Exit[E, A]` using `done` - each `await` will get this exit propagated + with result of effect `IO[E, A]` using `complete` - the effect will be executed once and the result will be propagated to all waiting fibers * with effect `IO[E, A]` using `completeWith` - first fiber that calls `completeWith` wins and sets the effect that **will be executed by each `await`ing fiber**, so be careful when using `p.completeWith(someEffect)` and rather use `p.complete(someEffect)` unless executing `someEffect` by each `await`ing fiber is intended * simply fail with `E` using `fail` * simply defect with `Throwable` using `die` * fail or defect with `Cause[E]` using `failCause` * interrupt with `interrupt` Following example shows usage of all of them: ```scala val race: IO[String, Int] = for { p <- Promise.make[String, Int] _ <- p.succeed(1).fork _ <- p.complete(ZIO.succeed(2)).fork _ <- p.completeWith(ZIO.succeed(3)).fork _ <- p.done(Exit.succeed(4)).fork _ <- p.fail("5") _ <- p.failCause(Cause.die(new Error("6"))) _ <- p.die(new Error("7")) _ <- p.interrupt.fork value <- p.await } yield value ``` The act of completing a Promise results in an `UIO[Boolean]`, where the `Boolean` represents whether the Promise value has been set (`true`) or whether it was already set (`false`). This is demonstrated below: ```scala val ioPromise1: UIO[Promise[Exception, String]] = Promise.make[Exception, String] val ioBooleanSucceeded: UIO[Boolean] = ioPromise1.flatMap(promise => promise.succeed("I'm done")) ``` Another example with `fail(...)`: ```scala val ioPromise2: UIO[Promise[Exception, Nothing]] = Promise.make[Exception, Nothing] val ioBooleanFailed: UIO[Boolean] = ioPromise2.flatMap(promise => promise.fail(new Exception("boom"))) ``` To re-iterate, the `Boolean` tells us whether or not the operation took place successfully (`true`) i.e. the Promise was set with the value or the error. ### Awaiting We can get a value from a Promise using `await`. The calling fiber will suspend until Promise is completed with a value or an error. ```scala val ioPromise3: UIO[Promise[Exception, String]] = Promise.make[Exception, String] val ioGet: IO[Exception, String] = ioPromise3.flatMap(promise => promise.await) ``` ### Polling If we don't want to suspend the fiber, but we only want to query the state of whether or not the Promise has been completed, we can use `poll`: ```scala val ioPromise4: UIO[Promise[Exception, String]] = Promise.make[Exception, String] val ioIsItDone: UIO[Option[IO[Exception, String]]] = ioPromise4.flatMap(p => p.poll) val ioIsItDone2: IO[Option[Nothing], IO[Exception, String]] = ioPromise4.flatMap(p => p.poll.some) ``` If the Promise was not completed when we called `poll` then the IO will fail with the `Unit` value. Otherwise, we obtain an `IO[E, A]`, where `E` represents if the Promise completed with an error and `A` indicates that the Promise successfully completed with an `A` value. `isDone` returns `UIO[Boolean]` that evaluates to `true` if Promise is already completed. ## Example Usage Here is a scenario where we use a `Promise` to hand over a value between two `Fiber`s: ```scala val program: ZIO[Any, IOException, Unit] = for { promise <- Promise.make[Nothing, String] sendHelloWorld = (ZIO.succeed("hello world") <* ZIO.sleep(1.second)).flatMap(promise.succeed) getAndPrint = promise.await.flatMap(Console.printLine(_)) fiberA <- sendHelloWorld.fork fiberB <- getAndPrint.fork _ <- (fiberA zip fiberB).join } yield () ``` In the example above, we create a Promise and a Fiber (`fiberA`) that completes the Promise after 1 second, and a second Fiber (`fiberB`) that will call `await` on that Promise to obtain a `String` and then print it to screen. The example prints `hello world` to the screen after 1 second. Remember, this is just a description of the program and not the execution itself. --- ## Queue `Queue` is a lightweight in-memory queue built on ZIO with composable and transparent back-pressure. It is fully asynchronous (no locks or blocking), purely-functional and type-safe. A `Queue[A]` contains values of type `A` and has two basic operations: `offer`, which places an `A` in the `Queue`, and `take` which removes and returns the oldest value in the `Queue`. ```scala val res: UIO[Int] = for { queue <- Queue.bounded[Int](100) _ <- queue.offer(1) v1 <- queue.take } yield v1 ``` ## Creating a queue A `Queue` can be bounded (with a limited capacity) or unbounded. There are several strategies to process new values when the queue is full: - The default `bounded` queue is back-pressured: when full, any offering fiber will be suspended until the queue is able to add the item - A `dropping` queue will drop new items when the queue is full - A `sliding` queue will drop old items when the queue is full To create a back-pressured bounded queue: ```scala val boundedQueue: UIO[Queue[Int]] = Queue.bounded[Int](100) ``` To create a dropping queue: ```scala val droppingQueue: UIO[Queue[Int]] = Queue.dropping[Int](100) ``` To create a sliding queue: ```scala val slidingQueue: UIO[Queue[Int]] = Queue.sliding[Int](100) ``` To create an unbounded queue: ```scala val unboundedQueue: UIO[Queue[Int]] = Queue.unbounded[Int] ``` ## Adding items to a queue The simplest way to add a value to the queue is `offer`: ```scala val res1: UIO[Unit] = for { queue <- Queue.bounded[Int](100) _ <- queue.offer(1) } yield () ``` When using a back-pressured queue, offer might suspend if the queue is full: you can use `fork` to wait in a different fiber. ```scala val res2: UIO[Unit] = for { queue <- Queue.bounded[Int](1) _ <- queue.offer(1) f <- queue.offer(1).fork // will be suspended because the queue is full _ <- queue.take _ <- f.join } yield () ``` It is also possible to add multiple values at once with `offerAll`: ```scala val res3: UIO[Unit] = for { queue <- Queue.bounded[Int](100) items = Range.inclusive(1, 10).toList _ <- queue.offerAll(items) } yield () ``` ## Consuming Items from a Queue The `take` operation removes the oldest item from the queue and returns it. If the queue is empty, this will suspend, and resume only when an item has been added to the queue. As with `offer`, you can use `fork` to wait for the value in a different fiber. ```scala val oldestItem: UIO[String] = for { queue <- Queue.bounded[String](100) f <- queue.take.fork // will be suspended because the queue is empty _ <- queue.offer("something") v <- f.join } yield v ``` You can consume the first item with `poll`. If the queue is empty you will get `None`, otherwise the top item will be returned wrapped in `Some`. ```scala val polled: UIO[Option[Int]] = for { queue <- Queue.bounded[Int](100) _ <- queue.offer(10) _ <- queue.offer(20) head <- queue.poll } yield head ``` You can consume multiple items at once with `takeUpTo`. If the queue doesn't have enough items to return, it will return all the items without waiting for more offers. ```scala val taken: UIO[Chunk[Int]] = for { queue <- Queue.bounded[Int](100) _ <- queue.offer(10) _ <- queue.offer(20) chunk <- queue.takeUpTo(5) } yield chunk ``` Similarly, you can get all items at once with `takeAll`. It also returns without waiting (an empty collection if the queue is empty). ```scala val all: UIO[Chunk[Int]] = for { queue <- Queue.bounded[Int](100) _ <- queue.offer(10) _ <- queue.offer(20) chunk <- queue.takeAll } yield chunk ``` ## Shutting Down a Queue It is possible with `shutdown` to interrupt all the fibers that are suspended on `offer*` or `take*`. It will also empty the queue and make all future calls to `offer*` and `take*` terminate immediately. ```scala val takeFromShutdownQueue: UIO[Unit] = for { queue <- Queue.bounded[Int](3) f <- queue.take.fork _ <- queue.shutdown // will interrupt f _ <- f.join // Will terminate } yield () ``` You can use `awaitShutdown` to execute an effect when the queue is shut down. This will wait until the queue is shut down. If the queue is already shut down, it will resume right away. ```scala val awaitShutdown: UIO[Unit] = for { queue <- Queue.bounded[Int](3) p <- Promise.make[Nothing, Boolean] f <- queue.awaitShutdown.fork _ <- queue.shutdown _ <- f.join } yield () ``` ## Additional Resources - [ZIO Queue Talk by John De Goes @ ScalaWave 2018](https://www.slideshare.net/jdegoes/zio-queue) - [ZIO Queue Talk by Wiem Zine El Abidine @ PSUG 2018](https://www.slideshare.net/wiemzin/psug-zio-queue) - [Elevator Control System using ZIO](https://medium.com/@wiemzin/elevator-control-system-using-zio-c718ae423c58) - [Scalaz 8 IO vs Akka (typed) actors vs Monix](https://blog.softwaremill.com/scalaz-8-io-vs-akka-typed-actors-vs-monix-part-1-5672657169e1) --- ## Ref `Ref[A]` models a **mutable reference** to a value of type `A` in which we can store **immutable** data. The two basic operations are `set`, which fills the `Ref` with a new value, and `get`, which retrieves its current content. `Ref` provides us a way to functionally manage in-memory state. All operations on `Ref` are atomic and thread-safe, giving us a reliable foundation for synchronizing concurrent programs. `Ref`: - is purely functional and referentially transparent - is concurrent-safe and lock-free - updates and modifies atomically ## Concurrent Stateful Application **`Ref` is the foundation for writing concurrent stateful applications**. Anytime we need to share information between multiple fibers, and those fibers have to update the same information, they need to communicate through something that provides the guarantee of atomicity. Because `Ref` is **concurrent-safe**, we can share the same `Ref` among many fibers. All of which can update `Ref` concurrently, removing the worry of race conditions. Even if we had ten thousand fibers all updating the same `Ref`, as long as they are using atomic update and modify functions, we will have zero race conditions. ## Operations Though `Ref` has many operations, here we will introduce the most common and important ones. ### make `Ref` is never empty, it always contains something. We can create a `Ref` by providing the initial value to its `make` method, a constructor of the `Ref` data type. We should pass an **immutable value** of type `A` to the constructor, and it returns an `UIO[Ref[A]]` value: ```scala def make[A](a: A): UIO[Ref[A]] ``` As we can see, the output is wrapped in`UIO`, which means creating a `Ref` is effectful. Whenever we `make`, `update`, or `modify` the `Ref`, we are performing an effectful operation. Let's create some `Ref`s from immutable values: ```scala val counterRef = Ref.make(0) // counterRef: UIO[Ref[Int]] = Sync( // trace = "repl.MdocSession.MdocApp.counterRef(ref.md:14)", // eval = zio.Ref$$$Lambda$19942/0x00007f02a6fa59c8@1e2f5fbf // ) val stringRef = Ref.make("initial") // stringRef: UIO[Ref[String]] = Sync( // trace = "repl.MdocSession.MdocApp.stringRef(ref.md:17)", // eval = zio.Ref$$$Lambda$19942/0x00007f02a6fa59c8@409b8d58 // ) sealed trait State case object Active extends State case object Changed extends State case object Closed extends State val stateRef = Ref.make(Active) // stateRef: UIO[Ref[Active.type]] = Sync( // trace = "repl.MdocSession.MdocApp.stateRef(ref.md:32)", // eval = zio.Ref$$$Lambda$19942/0x00007f02a6fa59c8@3808a7da // ) ``` > _**Warning**_: > > A big mistake when creating a `Ref` is trying to store mutable data inside it. A`Ref` must be used with **immutable data**. Otherwise, we lose our atomic guarantees, which can lead to collisions and race conditions. The following snippet compiles, but it leads to race conditions due to a mutable variable being provided to `make`: ```scala // Compiles but don't work properly val init = collection.mutable.Seq(1,3,5) // init: collection.mutable.Seq[Int] = ArrayBuffer(1, 3, 5) val counterRef = Ref.make(init) // counterRef: UIO[Ref[collection.mutable.Seq[Int]]] = Sync( // trace = "repl.MdocSession.MdocApp..counterRef(ref.md:42)", // eval = zio.Ref$$$Lambda$19942/0x00007f02a6fa59c8@1e6d987 // ) ``` To correct this, we should change the `init` to be immutable: ```scala val init = Seq(1,3,5) // init: Seq[Int] = List(1, 3, 5) val counterRef = Ref.make(init) // counterRef: UIO[Ref[Seq[Int]]] = Sync( // trace = "repl.MdocSession.MdocApp..counterRef(ref.md:52)", // eval = zio.Ref$$$Lambda$19942/0x00007f02a6fa59c8@5b3f7b64 // ) ``` ### get The `get` method returns the current value of the reference. Its return type is `IO[EB, B]` in which `B` is the value type of the effect and in the failure case, `EB` is the error type of that effect. ```scala def get: IO[EB, B] ``` As the `make` and `get` methods of `Ref` are effectful, we can chain them together with `flatMap`. In the following example, we create a `Ref` with `initial` value, and then we acquire the current state with the `get` method: ```scala Ref.make("initial") .flatMap(_.get) .flatMap(current => Console.printLine(s"current value of ref: $current")) ``` We can refactor this to use a for-comprehension rather than a series of `flatMap`s to increase readability: ```scala for { ref <- Ref.make("initial") value <- ref.get } yield assert(value == "initial") ``` Note that, there is no way to access the shared state outside the monadic operations. ### set The `set` method atomically writes a new value to the `Ref`. ```scala for { ref <- Ref.make("initial") _ <- ref.set("update") value <- ref.get } yield assert(value == "update") ``` ### update With `update`, we can atomically update the state of `Ref` with a given **pure** function, that is, it needs to be deterministic and free of side effects. ```scala def update(f: A => A): IO[E, Unit] ``` Assume we have a counter, we can increase its value with the `update` method: ```scala val counterInitial = 0 for { counterRef <- Ref.make(counterInitial) _ <- counterRef.update(_ + 1) value <- counterRef.get } yield assert(value == 1) ``` :::caution `update` is not the composition of `get` and `set`. This composition is not concurrent-safe. Whenever we need to update our state, we should use the `update` operation which modifies its `Ref` atomically. ::: For example, the following snippet is not concurrent-safe: ```scala // Unsafe State Management object UnsafeCountRequests extends ZIOAppDefault { def request(counter: Ref[Int]) = for { current <- counter.get _ <- counter.set(current + 1) } yield () private val initial = 0 private val myApp = for { ref <- Ref.make(initial) _ <- request(ref) zipPar request(ref) rn <- ref.get _ <- Console.printLine(s"total requests performed: $rn") } yield () def run = myApp } ``` The above snippet doesn't behave deterministically. This program sometimes prints `2` and sometimes prints `1`. We can fix it by using `update`: ```scala // Safe State Management object CountRequests extends ZIOAppDefault { def request(counter: Ref[Int]): ZIO[Any, Nothing, Unit] = { for { _ <- counter.update(_ + 1) reqNumber <- counter.get _ <- Console.printLine(s"request number: $reqNumber").orDie } yield () } private val initial = 0 private val myApp = for { ref <- Ref.make(initial) _ <- request(ref) zipPar request(ref) rn <- ref.get _ <- Console.printLine(s"total requests performed: $rn").orDie } yield () def run = myApp } ``` Here is another use case of `update` to write a `repeat` combinator: ```scala def repeat[E, A](n: Int)(io: IO[E, A]): IO[E, Unit] = Ref.make(0).flatMap { iRef => def loop: IO[E, Unit] = iRef.get.flatMap { i => if (i < n) io *> iRef.update(_ + 1) *> loop else ZIO.unit } loop } ``` ### modify `modify` is a more powerful version of `update`. It atomically modifies `Ref` by the given function, and also computes a return value. The function that we pass to `modify` needs to be a pure function; it needs to be deterministic and free of side effects. ```scala def modify[B](f: A => (B, A)): IO[E, B] ``` Remember the `CountRequest` example. What if we want to log the number of each request inside the `request` function? Let's see what happens if we write that function with the composition of `update` and `get` methods: ```scala // Unsafe in Concurrent Environment def request(counter: Ref[Int]) = { for { _ <- counter.update(_ + 1) rn <- counter.get _ <- Console.printLine(s"request number received: $rn") } yield () } ``` What happens if, between running `update` and `get`, a second `update` occurs on another fiber? This would not behave deterministically in concurrent environments. So we need a way to perform a combination of **get, set, get** atomically. This is where `modify` comes in. Here we will edit `request` to use `modify`: ```scala // Safe in Concurrent Environment def request(counter: Ref[Int]) = { for { rn <- counter.modify(c => (c + 1, c + 1)) _ <- Console.printLine(s"request number received: $rn") } yield () } ``` ## AtomicReference in Java For Java programmers, we can think of `Ref` as an `AtomicReference`. Java has a `java.util.concurrent.atomic` package which contains `AtomicReference`, `AtomicLong`, `AtomicBoolean` and so forth. `Ref` has roughly the same power, guarantees, and limitations as `AtomicReference`, but is higher-level and ZIO-friendly. ## Ref vs. State Monad Basically `Ref` allows us to have all the power of State Monad inside ZIO. State Monad lacks two important features that we use in real-life application development: 1. Concurrency Support 2. Error Handling ### Concurrency State Monad is an effect system that only includes state. It allows us to do pure stateful computations. We can only get, set, and update (and related computations) state. State Monad updates its state with series of stateful computations sequentially, but **it can't be used to do async or concurrent computations**. `Ref`, in contrast, has great support for concurrent and async programming. ### Error Handling In most real-life,stateful applications, we will involve some database IO and API calls and/or some concurrent and sync operations which can fail in different ways along the path of execution. So besides state management, we need a way to handle errors. The State Monad doesn't have the ability to model error management. We can combine State Monad and Either Monad with StateT monad transformer, but it imposes massive performance overhead. It doesn't buy us anything that we can't do with `Ref`. So it is an anti-pattern. In the ZIO model, errors are encoded in effects and `Ref` utilizes that. So, in addition to state management, we have the ability to handle errors without additional work. ## State Transformers Those who live on the dark side of mutation sometimes have it easy; they can add state everywhere like it's Christmas. Behold: ```scala var idCounter = 0 def freshVar: String = { idCounter += 1 s"var${idCounter}" } val v1 = freshVar val v2 = freshVar val v3 = freshVar ``` As functional programmers, we know better and have captured state mutation in the form of functions of type `S => (A, S)`. `Ref` provides such an encoding, with `S` being the type of the value, and `modify` embodying the state mutation function. ```scala Ref.make(0).flatMap { idCounter => def freshVar: UIO[String] = idCounter.modify(cpt => (s"var${cpt + 1}", cpt + 1)) for { v1 <- freshVar v2 <- freshVar v3 <- freshVar } yield () } ``` ## Building more sophisticated concurrency primitives `Ref` is low-level enough that it can serve as the foundation for other concurrency data types. For example, semaphores are a classic abstract data type for controlling access to shared resources. They are defined as a triplet `S = (v, P, V)` where `v` is the number of units of the resource that are currently available, and `P` and `V` are operations that decrement and increment `v`, respectively. `P` will only complete when `v` is non-negative and must wait if it isn't. With `Ref`, it's easy to implement such a semaphore! The only difficulty is in `P`, where we must fail and retry when either `v` is negative, or its value has changed between the moment we read it and the moment we try to update it. A naive implementation could look like: ```scala sealed trait S { def P: UIO[Unit] def V: UIO[Unit] } object S { def apply(v: Long): UIO[S] = Ref.make(v).map { vref => new S { def V = vref.update(_ + 1).unit def P = (vref.get.flatMap { v => if (v < 0) ZIO.fail(()) else vref.modify(v0 => if (v0 == v) (true, v - 1) else (false, v)).flatMap { case false => ZIO.fail(()) case true => ZIO.unit } } <> P).unit } } } ``` Let's rock these crocodile boots we found the other day at the market and test our semaphore at the night club, yee-haw: ```scala val party = for { dancefloor <- S(10) dancers <- ZIO.foreachPar(1 to 100) { i => dancefloor.P *> Random.nextDouble.map(d => Duration.fromNanos((d * 1000000).round)).flatMap { d => printLine(s"${i} checking my boots") *> ZIO.sleep(d) *> printLine(s"${i} dancing like it's 99") } *> dancefloor.V } } yield () ``` It goes without saying you should take a look at ZIO's own `Semaphore`, it does all this and more without wasting all those CPU cycles while waiting. ## Atomic Modify with Continuation As discussed above, `Ref#modify` takes a **pure function** to atomically modify the state and compute a return value. While we shouldn't run effects inside `Ref#modify`, we can introduce a continuation—an effect that the runtime executes *after* the atomic modification: ```scala ref.modify { state => val doThisNext = someZIO val newState = computeNewState(state) (doThisNext, newState) // "doThisNext" is the continuation }.flatten // flatten to execute the continuation ``` The type of `ref.modify { state => ... }` is an effect that produces another effect. Running this effect performs the atomic modification and returns the continuation effect. To execute the continuation, we must `flatten` the result. **Important:** The continuation is **not** part of the atomic modification. The state modification completes before the continuation runs, and it doesn't depend on the continuation's result. If multiple fibers execute this pattern concurrently, their continuations may be interleaved, even though their state modifications are atomic. If you need the continuation to be part of the atomic operation—ensuring no other fiber can modify the state between the modification and the continuation—use `Ref.Synchronized` instead. See [Ref.Synchronized](refsynchronized.md) for more details. --- ## Ref.Synchronized `Ref.Synchronized[A]` models a **mutable reference** to a value of type `A` in which we can store **immutable** data, and update it atomically **and** effectfully. :::note Almost all of `Ref.Synchronized` operations are the same as `Ref`. We suggest reading [`Ref`](ref.md) at first if you are not familiar with `Ref`. ::: Let's explain how we can update a shared state effectfully with `Ref.Synchronized`. The `update` method and all other related methods get an effectful operation, and then they run these effects to change the shared state. This is the main difference between `Ref.Synchronized` and `Ref`. In the following example, we should pass in `updateEffect` to it which is the description of an update operation. So `Ref.Synchronized` is going to update the `ref` by running the `updateEffect`: ```scala for { ref <- Ref.Synchronized.make("current") updateEffect = ZIO.succeed("update") _ <- ref.updateZIO(_ => updateEffect) value <- ref.get } yield assert(value == "update") ``` In real-world applications, there are cases where we want to run an effect, e.g. query a database, and then update the shared state. This is where `Ref.Synchronized` can help us to update the shared state in a more actor model fashion. We have a shared mutable state but for every different command or message, and we want execute our effect and update the state. We can pass in an effectful program into every single update. All of them will be done parallel, but the result will be sequenced in such a fashion that they only touched the state at different times, and we end up with a consistent state at the end. In the following example, we are going to send `getAge` request to usersApi for each user and updating the state respectively: ```scala val meanAge = for { ref <- Ref.Synchronized.make(0) _ <- ZIO.foreachPar(users) { user => ref.updateZIO(sumOfAges => api.getAge(user).map(_ + sumOfAges) ) } v <- ref.get } yield (v / users.length) ``` --- ## Semaphore A `Semaphore` datatype which allows synchronization between fibers with the `withPermit` operation, which safely acquires and releases a permit. `Semaphore` is based on `Ref[A]` datatype. ## Operations For example, a synchronization of asynchronous tasks can be done via acquiring and releasing a semaphore with a given number of permits it can spend. When the acquire operation cannot be performed due to no more available `permits` in the semaphore, such task is semantically blocked, until the `permits` value is large enough again: ```scala val task = for { _ <- printLine("start") _ <- ZIO.sleep(Duration(2, TimeUnit.SECONDS)) _ <- printLine("end") } yield () val semTask = (sem: Semaphore) => for { _ <- sem.withPermit(task) } yield () val semTaskSeq = (sem: Semaphore) => (1 to 3).map(_ => semTask(sem)) val program = for { sem <- Semaphore.make(permits = 1) seq <- ZIO.succeed(semTaskSeq(sem)) _ <- ZIO.collectAllPar(seq) } yield () ``` As the binary semaphore is a special case of a counting semaphore, we can acquire and release any number of `permits`: ```scala val semTaskN = (sem: Semaphore) => for { _ <- sem.withPermits(5)(task) } yield () ``` The guarantee of `withPermit` (and its corresponding counting version `withPermits`) is that each acquisition will be followed by the equivalent number of releases, regardless of whether the task succeeds, fails, or is interrupted. --- ## Introduction to Configuration in ZIO Configuration is a core concern for any cloud-native application. So ZIO ships with built-in support for configuration by providing a front-end for configuration providers as well as metrics and logging. So, ZIO provides a unified way to configure our applications, while still enabling customizability, flexibility, and significant integrations with configuration backends via ecosystem projects, most notably [ZIO Config](https://zio.dev/zio-config). This configuration front-end allows ecosystem libraries and applications to declaratively describe their configuration needs and delegates the heavy lifting to a ConfigProvider, which may be supplied by third-party libraries such as ZIO Config. The ZIO Core ships with a simple default config provider, which reads configuration data from environment variables and if not found, from system properties. This can be used for development purposes or to bootstrap applications toward more sophisticated config providers. ## Getting Started To make our application configurable, we should know about three essential elements: 1. **Config Description**— To describe configuration data of type `A`, we should create an instance of `Config[A]`. If the configuration data is simple (such as `string`, `string`, `boolean`), we can use built-in configs inside companion object of `Config` data type. By combining primitive configs, we can model custom data types such as `HostPort`. 2. **Config Front-end**— By using `ZIO.config` we can load configuration data described by `Config`. It takes a `Config[A]` instance or expect implicit `Config[A]` and loads the config using the current `ConfigProvider`. 3. **Config Backend**— `ConfigProvider` is the underlying engine that `ZIO.config` uses to load configs. ZIO has a default config provider inside its default services. The default config provider reads configuration data from environment variables and if not found, from system properties. To change the default config provider, we can use `Runtime.setConfigProvider` layer to configure the ZIO runtime to use a custom config provider. :::note By introducing built-in config front-end in ZIO Core, the old way of reading configuration data using `ZLayer` is deprecated, and we don't recommend using layers for configuration anymore. ::: ## Primitive Configs ZIO provides a set of primitive configs for the most common types like `int`, `long`, `string`, `boolean`, `double`, etc. All of these configs are available inside the `Config` object. Let's start with a simple example of how to read configuration from environment variables and system properties: ```scala object MainApp extends ZIOAppDefault { def run = { for { host <- ZIO.config(Config.string("host")) port <- ZIO.config(Config.int("port")) _ <- Console.printLine(s"Application started: $host:$port") } yield () } } ``` :::note Use `ZIO.config(config: A)` overload for primitive data types instead of `ZIO.config[A]` to avoid potential implicit conflicts. ::: If we run this application we will get the following output: ```bash timestamp=2023-02-14T09:45:27.074151Z level=ERROR thread=#zio-fiber-0 message="" cause="Exception in thread "zio-fiber-4" zio.Config$Error$Or: ((Missing data at host: Expected HOST to be set in the environment) or (Missing data at host: Expected host to be set in properties)) ``` This is because we have not provided any configuration. Let's try to run it with the following environment variables: ```bash HOST=localhost PORT=8080 sbt "runMain MainApp" ``` Now we get the following output: ```bash Application started: localhost:8080 ``` We can also run it by setting system properties: ```bash sbt -Dhost=localhost -Dport=8080 "runMain MainApp" ``` ## Custom Configs Other than primitive types, we can also define a configuration for custom types. To do so, we need to use primitive configs and combine them together using `Config` operators (`++`, `||`, `map`, etc) and constructors (`listOf`, `chunkOf`, `setOf`, `vectorOf`, `table`, etc). ### Example 1 Let's say we have the `HostPort` data type, which consists of two fields: `host` and `port`: ```scala case class HostPort(host: String, port: Int) ``` We can define implicit config for this data type by combining primitive `string` and `int` configs: ```scala object HostPort { implicit val config: Config[HostPort] = (Config.string("host") ++ Config.int("port")).map { case (host, port) => HostPort(host, port) } } ``` :::note The best practice is to put the implicit `Config` value in the companion object of the configuration data type and call it `config`. ::: If we use this customized config in our application, it tries to read corresponding values from environment variables (`HOST` and `PORT`) and system properties (`host` and `port`): ```scala for { config <- ZIO.config[HostPort] _ <- Console.printLine(s"Application started: $config") } yield () ``` ### Example 2 Now let's assume we want to have multiple `HostPort` configurations. We can define a config for a list of `HostPort` like bellow using the `listOf` constructor: ```scala case class HostPorts(hostPorts: List[HostPort]) object HostPorts { implicit val config: Config[HostPorts] = Config.listOf(HostPort.config).map(HostPorts(_)) } ``` Then we can use this config in our application: ```scala for { config <- ZIO.config[HostPorts] _ <- Console.printLine(s"Application started with:") _ <- ZIO.foreachDiscard(config.hostPorts)(e => Console.printLine(s" - http://${e.host}:${e.port}")) } yield () ``` With the default config provider, we can run the application with the following environment variables: ```bash HOST=host1,host2,host3 PORT=8080,8081,8082 sbt "runMain MainApp" ``` The output will be: ```bash Application started with: - http://host1:8081 - http://host2:8082 - http://host3:8083 ``` ## Top-level and Nested Configs So far we have seen how to define configuration in a top-level manner, whether it is a primitive or a custom type. But we can also define a nested configuration. Assume we have a `SerivceConfig` data type that consists of two fields: `hostPort` and `timeout`: ```scala case class ServiceConfig(hostPort: HostPort, timeout: Int) ``` Let's define a config for this type in its companion object: ```scala object ServiceConfig { implicit val config: Config[ServiceConfig] = (HostPort.config ++ Config.int("timeout")).map { case (a, b) => ServiceConfig(a, b) } } ``` If we use this customized config in our application, it tries to read corresponding values from environment variables (`HOST`, `PORT`, and `TIMEOUT`) and, if not found from system properties (`host`, `port`, and `timeout`). But in most circumstances, we don't want to read all the configurations from the top-level namespace. Instead, we want to nest them under a common namespace. In this case, we want to read both `HOST` and `PORT` from the `HOSTPORT` namespace, and `TIMEOUT` from the root namespace. In order to do that, we can use the `nested` combinator: ```diff object ServiceConfig { implicit val config: Config[ServiceConfig] = - (HostPort.config ++ Config.int("timeout")).map { + (HostPort.config.nested("hostport") ++ Config.int("timeout")).map { case (a, b) => ServiceConfig(a, b) } } ``` Now, if we run our application, it tries to read corresponding values from environment variables (`HOSTPORT_HOST`, `HOSTPORT_PORT` and `TIMEOUT`) and, if not found it tries to read from system properties (`hostport.host`, `hostport.port` and `timeout`). ## Built-in Config Providers ZIO has some built-in config providers: - `ConfigProvider.defaultProvider` - reads configuration from environment variables and if not found, from system properties - `ConfigProvider.envProvider` - reads configuration from environment variables - `ConfigProvider.propsProvider` - reads configuration from system properties - `ConfigProvider.consoleProvider` - reads configuration from interactive console prompts Other than these built-in providers, we can also use third-party providers in ZIO ecosystem libraries, such as ZIO Config which provides a rich set of backends for reading configuration from different sources such as HOCON, JSON, YAML, etc. ## Custom Config Provider We can also define our own custom config providers. The default config provider is used by default, but we can also override it by using `Runtime#setConfigProvider`. In the following example, we set the default config provider to `consoleProvider` which reads configuration from the console: ```scala object MainAppScoped extends ZIOAppDefault { override val bootstrap: ZLayer[Any, Nothing, Unit] = Runtime.setConfigProvider(ConfigProvider.consoleProvider) def run = for { host <- ZIO.config(Config.string("host")) port <- ZIO.config(Config.int("port")) _ <- Console.printLine(s"Application started: http://$host:$port") } yield () } ``` :::note The console provider is stored inside a `FiberRef`, so we can override it in a scoped manner. This is useful for changing the config provider for a specific part of the application. ::: ## Testing Services When testing services, we sometimes need to provide some configuration to them. So we should be able to mock any backend that we use for reading configuration data. In order to do that, we can use the `ConfigProvider.fromMap` constructor, which takes a map of configuration data and returns a config provider that reads configuration from that map. Then we can pass that to the `Runtime.setConfigProvider`, which returns a `ZLayer` that we can use to override the default config provider for our test specs using `Spec#provideLayer` operator: ```scala object MyServiceTest extends ZIOSpecDefault { val mockConfigProvider: ZLayer[Any, Nothing, Unit] = Runtime.setConfigProvider(ConfigProvider.fromMap(Map("timeout" -> "5s"))) // This service reads configuration data (host and port) inside its implementation def myService: ZIO[Any, Config.Error, Double] = ??? override def spec = { val expected: Double = ??? // expected value test("test myService") { for { result <- myService } yield assertTrue(result == expected) } }.provideLayer(mockConfigProvider) } ``` --- ## Automatic ZLayer Derivation ZIO's `ZLayer` is a powerful tool for building modular, testable, and composable applications. With the `ZLayer.derive` utility, you can automatically derive simple `ZLayer` instances for your services, reducing boilerplate and simplifying your codebase. ## Basic Use Cases ```scala class Database(connection: String) object Database { val layer: ZLayer[String, Nothing, Database] = ZLayer.derive[Database] } class UserService(db: Database) object UserService { val layer: ZLayer[Database, Nothing, UserService] = ZLayer.derive[UserService] } ``` ## Default Values For services that might have default values or configurations, `ZLayer.derive` can use implicit `ZLayer.Derive.Default[A]` values: ### Pre-defined Default Values There are some pre-defined `ZLayer.Derive.Default[A]` instances for the following types: #### `Config[A]` When a service `A` has a constructor parameter `B` and there's an implicit `Config[B]` instance, `ZLayer.derive` automatically loads `B` using `ZIO.config`. ```scala case class APIClientConfig(appKey: String, secretKey: Config.Secret) object APIClientConfig { // Because we have an implicit `Config[APIClientConfig]` in scope... implicit val config: Config[APIClientConfig] = (Config.string("appKey") ++ Config.secret("secretKey")).map { case (uri, key) => APIClientConfig(uri, key) } } class APIClient(config: APIClientConfig) { /* ... */ } object APIClient { // `APIClientConfig` is automatically loaded using `ZIO.config` by `ZLayer.derive`, // instead of being required as a layer input. val layer: ZLayer[Any, Config.Error, APIClient] = ZLayer.derive[APIClient] } ``` Refer to [Configuration](../configuration/index.md) for more about `Config`. #### Some Concurrency Primitives - `Promise[E, A]` - `Queue[A]` (using `Queue.unbounded`) - `Hub[A]` (using `Hub.unbounded`) - `Ref[A]` (when `A` has a default instance) ### Creating New Default Value There are three main ways to create a `ZLayer.Derive.Default`: 1. `ZLayer.Derive.Default.succeed` for creating default values from simple values. 2. `ZLayer.Derive.Default.fromZIO` for creating default values from effects. 3. `ZLayer.Derive.Default.fromLayer` for creating default values from layers. ### Overriding Predefined Default Values At times, you may want to override a default value in specific scenarios. To achieve this, you can define your own implicit value in a scope with a higher implicit priority, like a closer lexical scope. A common scenario for this is when you want to discard a pre-defined default value and instead treat it as a dependency. Use `ZLayer.Derive.Default.service` for this purpose: ```scala class Wheels(number: Int) object Wheels { implicit val defaultWheels: Default.WithContext[Any, Nothing, Wheels] = Default.succeed(new Wheels(4)) } class Car(wheels: Wheels) val carLayer1: ZLayer[Any, Nothing, Car] = ZLayer.derive[Car] // wheels.number == 4 val carLayer2: ZLayer[Wheels, Nothing, Car] = locally { // The default instance is discarded implicit val newWheels: Default.WithContext[Wheels, Nothing, Wheels] = Default.service[Wheels] ZLayer.derive[Car] } ``` ### Caveat: Use `Default.WithContext[R, E, A]` instead of `Default[A]` for type annotation When providing type annotations for `ZLayer.derive`, you must use `ZLayer.Derive.Default.WithContext[R, E, A]` instead of the more general `ZLayer.Derive.Default[A]`. Using the latter will result in a compilation error due to missing type details. If you're uncertain about the exact type signature, a practical approach is to omit the type annotation initially. Then, use your IDE's autocomplete feature to insert the inferred type. ## Attaching Scoped Resources For services requiring resource management, `ZLayer.derive` offers built-in support for scoped values. When a service `A` implements the `ZLayer.Derive.Scoped[-R, +E]` trait, `ZLayer.derive[A]` automatically recognizes it. As a result, the `scoped` effect is executed during the layer's construction and finalization phases. The 'resource' might be a background task, a lock file, or etc., that can be managed by [`Scope`](../resource/scope.md). ```scala trait Connection { def healthCheck: ZIO[Any, Throwable, Unit] // ... } class ThirdPartyService(connection: Connection) extends ZLayer.Derive.Scoped[Any, Nothing] { // Repeats health check every 10 seconds in background during the layer's lifetime override def scoped(implicit trace: Trace): ZIO[Scope, Nothing, Any] = connection.healthCheck .ignoreLogged .repeat(Schedule.spaced(10.seconds)) .forkScoped } object ThirdPartyService { // `ZLayer.Derive.Scoped` should be used with `ZLayer.derive` val layer: ZLayer[Connection, Nothing, ThirdPartyService] = ZLayer.derive[ThirdPartyService] } ``` If `scoped` fails during resource acquisition, the entire `ZLayer` initialization process fails. ### Lifecycle Hooks Additionally, there's the `ZLayer.Derive.AcquireRelease[R, E, A]` trait. This is a specialized version of `ZLayer.Derive.Scoped` designed for added convenience, allowing users to define initialization and finalization hooks distinctly. ```scala def acquireLockFile(path: String): ZIO[Any, Throwable, File] = ??? def deleteFile(file: File): ZIO[Any, Throwable, Unit] = ??? class ASingletonService(lockFilePath: String) extends ZLayer.Derive.AcquireRelease[Any, Throwable, File] { override def acquire: ZIO[Any, Throwable, File] = acquireLockFile(lockFilePath) override def release(lockFile: File): ZIO[Any, Nothing, Any] = deleteFile(lockFile).ignore } object ASingletonService { // Note: it's for illustrative example. In a real-world application, you will probably want to // put the `String` in a config. val layer: ZLayer[String, Throwable, ASingletonService] = ZLayer.derive[ASingletonService] } ``` ### Caveat: Manual layers do not respect `ZLayer.Derive.Scoped` and `ZLayer.Derive.AcquireRelease` When manually creating `ZLayer` instances without using `ZLayer.derive`, the lifecycle hooks won't be automatically invoked. Refer to [Resource Management in ZIO](../resource/index.md) for more details about general resource management in ZIO. --- ## Introduction to the ZIO's Contextual Data Types ZIO provides a contextual abstraction that encodes the environment of the running effect. This means, every effect can work within a specific context, called an environment. So when we have a `ZIO[R, E, A]` effect, we can say "given `R` as the environment of the effect, the effect may fail with an error type of `E`, or may succeed with a value of type `A`". For example, when we have an effect of type `ZIO[DatabaseConnection, IOException, String]`, we can say that our effect works within the context of `DatabaseConnection`. In other words, we can say that our effect requires the `DatabaseConnection` service as a context to run. We will see how layers can be used to eliminate the environment of an effect: ```scala trait DatabaseConnection // An effect which requires DatabaseConnection to run val effect: ZIO[DatabaseConnection, IOException, String] = ??? // A layer that produces DatabaseConnection service val dbConnection: ZLayer[Any, IOException, DatabaseConnection] = ??? // After applying dbConnection to our environmental effect the reurned // effect has no dependency on the DatabaseConnection val eliminated: ZIO[Any, IOException, String] = dbConnection { // Provides DatabaseConnection context effect // An effect running within `DatabaseConnection` context } ``` ZIO provides this facility through the following concepts and data types: 1. [ZIO Environment](#1-zio-environment) — The `R` type parameter of `ZIO[R, E, A]` data type. 2. [ZEnvironment](#2-zenvironment) — Built-in type-level map for maintaining the environment of a `ZIO` data type. 3. [ZLayer](#3-zlayer) — Describes how to build one or more services in our application. Next, we will discuss _ZIO Environment_ and _ZLayer_ and finally how to write ZIO services using the _Service Pattern_. ## 1. ZIO Environment The `ZIO[-R, +E, +A]` data type describes an effect that requires an input of type `R`, as an environment, may fail with an error of type `E`, or succeed with a value of type `A`. The input type is also known as _environment type_. This type-parameter indicates that to run an effect we need one or some services as an environment of that effect. In other word, `R` represents the _requirement_ for the effect to run, meaning we need to fulfill the requirement in order to make the effect _runnable_. So we can think of `ZIO[R, E, A]` as a mental model of a function from a value of type `R` to the `Either[E, A]`: ```scala type ZIO[R, E, A] = R => Either[E, A] ``` `R` represents dependencies; whatever services, config, or wiring a part of a ZIO program depends upon to work. We will explore what we can do with `R`, as it plays a crucial role in `ZIO`. ### Motivation One might ask "What is the motivation behind encoding the dependency in the type parameter of `ZIO` data type"? What is the benefit of doing so? Let's see how writing an application which requires reading from or writing to the terminal. As part of making the application _modular_ and _testable_ we define a separate service called `Terminal` which is responsible for reading from and writing to the terminal. We do that simply by writing an interface: ```scala trait Terminal { def print(line: Any): Task[Unit] def printLine(line: Any): Task[Unit] def readLine: Task[String] } ``` Now we can write our application that accepts the `Terminal` interface as a parameter: ```scala def myApp(c: Terminal): Task[Unit] = for { _ <- c.print("Please enter your name: ") name <- c.readLine _ <- c.printLine(s"Hello, $name!") } yield () ``` Similar to the object-oriented paradigm we code to interface not implementation. In order to run the application, we need to implement a production version of the `Terminal`: ```scala object TerminalLive extends Terminal { override def print(line: Any): Task[Unit] = ZIO.attemptBlocking(scala.Predef.print(line)) override def printLine(line: Any): Task[Unit] = ZIO.attemptBlocking(scala.Predef.println(line)) override def readLine: Task[String] = ZIO.attemptBlocking(scala.io.StdIn.readLine()) } ``` Finally, we can provide the `TerminalLive` to our application and run the whole: ```scala object MainApp extends ZIOAppDefault { def myApp(c: Terminal): Task[Unit] = for { _ <- c.print("Please enter your name: ") name <- c.readLine _ <- c.printLine(s"Hello, $name!") } yield () def run = myApp(TerminalLive) } ``` In the above example, we discard the fact that we could use the ZIO environment and utilize the `R` parameter of the `ZIO` data type. So instead we tried to write the application with the `Task` data type, which ignores the ZIO environment. To create our application testable, we gathered all terminal functionalities into the same interface called `Terminal`, and implemented that in another object called `TerminalLive`. Finally, at the end of the day, we provide the implementation of the `Terminal` service, i.e. `TerminalLive`, to our application. **While this technique works for small programs, it doesn't scale.** Assume we have multiple services, and we use them in our application logic like below: ```scala def foo( s1: Service1, s2: Service2, s3: Service3 )(arg1: String, arg2: String, arg3: Int): Task[Int] = ??? def bar( s1: Service1, s12: Service12, s18: Service18, sn: ServiceN )(arg1: Int, arg2: String, arg3: Double, arg4: Int): Task[Unit] def myApp(s1: Service1, s2: Service2, ..., sn: ServiceN): Task[Unit] = for { a <- foo(s1, s2, s3)("arg1", "arg2", 4) _ <- bar(s1, s12, s18, sn)(7, "arg2", 1.2, a) ... } yield () ``` Writing real applications using this technique is tedious and cumbersome because all dependencies have to be passed across all methods. We can simplify the process of writing our application by using the ZIO environment and [Service Pattern](../service-pattern/service-pattern.md). ```scala def foo(arg1: String, arg2: String, arg3: Int): ZIO[Service1 & Service2 & Service3, Throwable, Int] = for { s1 <- ZIO.service[Service1] s2 <- ZIO.service[Service2] ... } yield () def bar(arg1: Int, arg2: String, arg3: Double, arg4: Int): ZIO[Service1 & Service12 & Service18 & ServiceN, Throwable, Unit] = for { s1 <- ZIO.service[Service1] s12 <- ZIO.service[Service12] ... } yield () ``` ### Advantage of Using ZIO Environment ZIO environment facility enables us to: 1. **Code to Interface** — Like object-oriented paradigm, in ZIO we are encouraged to code to interface and defer the implementation. It is the best practice, but ZIO does not enforce us to do that. 2. **Write a Testable Code** — By coding to an interface, whenever we want to test our effects, we can easily mock any external services, by providing a _test_ version of those instead of the _live_ version. 3. **Compose Services with Strong Type Inference Facility** — We can compose multiple effects that require various services, so the final effect requires the intersection of all those services: ```scala trait ServiceA trait ServiceB trait ServiceC // Requires ServiceA and produces a value of type Int def foo: ZIO[ServiceA, Nothing, Int] = ??? // Requires ServiceB and ServiceC and produces a value of type String def bar: ZIO[ServiceB & ServiceC, Throwable, String] = ??? // Requires ServicB and produces a value of type Double def baz(a: Int, b: String): ZIO[ServiceB, Nothing, Double] = ??? // Requires ServiceA and ServiceB and ServiceC and produces a value of type Double val myApp: ZIO[ServiceA & ServiceB & ServiceC, Throwable, Double] = for { a <- foo b <- bar c <- baz(a, b) } yield c ``` Another important note about the ZIO environment is that the type inference works well on effect composition. After we composed all the application logic together, the compiler and also IDE can infer the proper type for the environment of the final effect. In the example above, the compiler can infer the environment type of the `myApp` effect which is `ServiceA & ServiceB & ServiceC`. ### Accessing ZIO Environment We have two types of accessors for the ZIO environment: 1. **Service Accessor (`ZIO.service`)** is used to access a specific service from the environment. 2. **Service Member Accessors (`ZIO.serviceWith` and `ZIO.serviceWithZIO`)** are used to access capabilities of a specific service from the environment. :::note To access the entire ZIO environment we can use `ZIO.environment*`, but we do not use these methods regularly to access ZIO services. Instead, we use service accessors and service member accessors. ::: #### Service Accessor To access a service from the ZIO environment, we can use the `ZIO.service` constructor. For example, in the following program we are going to access the `AppConfig` from the environment: ```scala case class AppConfig(host: String, port: Int) val myApp: ZIO[AppConfig, Nothing, Unit] = for { config <- ZIO.service[AppConfig] _ <- ZIO.logInfo(s"Application started with config: $config") } yield () ``` To run the `myApp` effect, we should provide the `AppConfig` layer (we will talk about `ZLayer` on the next section): ```scala object MainApp extends ZIOAppDefault { def run = myApp.provide(ZLayer.succeed(AppConfig("localhost", 8080))) } ``` To access multiple services from the ZIO environment, we can do the same: ```scala trait Foo trait Bar trait Baz for { foo <- ZIO.service[Foo] bar <- ZIO.service[Bar] bax <- ZIO.service[Baz] } yield () ``` When creating ZIO layers that have multiple dependencies, this can be helpful. We will discuss this pattern in the [Service Pattern](../service-pattern/service-pattern.md) section. #### Service Member Accessors Sometimes instead of accessing a service, we need to access the capabilities (members) of a service. Based on the return type of each capability, we can use one of these accessors: - **ZIO.serviceWith** - **ZIO.serviceWithZIO** In [Service Pattern](../service-pattern/service-pattern.md), we use these accessors to write "accessor methods" for ZIO services. Let's look at each one in more detail: 1. **ZIO.serviceWith** — When we are accessing service members whose return type is an ordinary value, we should use the `ZIO.serviceWith`. In the following example, we need to use the `ZIO.serviceWith` to write accessor methods for all of the `AppConfig` members: ```scala case class AppConfig(host: String, port: Int, poolSize: Int) object AppConfig { // Accessor Methods def host: ZIO[AppConfig, Nothing, String] = ZIO.serviceWith(_.host) def port: ZIO[AppConfig, Nothing, Int] = ZIO.serviceWith(_.port) def poolSize: ZIO[AppConfig, Nothing, Int] = ZIO.serviceWith(_.poolSize) } val myApp: ZIO[AppConfig, Nothing, Unit] = for { host <- AppConfig.host port <- AppConfig.port _ <- ZIO.logInfo(s"The service will be service at $host:$port") poolSize <- AppConfig.poolSize _ <- ZIO.logInfo(s"Application started with $poolSize pool size") } yield () ``` 2. **ZIO.serviceWithZIO** — When we are accessing service members whose return type is a ZIO effect, we should use the `ZIO.serviceWithZIO`. For example, in order to write the accessor method for the `foo` member of the `Foo` service, we need to use the `ZIO.serviceWithZIO` function: ```scala trait Foo { def foo(input: String): Task[Unit] } object Foo { // Accessor Method def foo(input: String): ZIO[Foo, Throwable, Unit] = ZIO.serviceWithZIO(_.foo(input)) } ``` ## 2. ZEnvironment `ZEnvironment` is a built-in type-level map for maintaining the environment of a `ZIO` data type. We don't typically use this data type directly. It's okay to skip learning it at the moment. We have a [separate article](zenvironment.md) about this data type. ## 3. ZLayer `ZLayer[-RIn, +E, +ROut]` is a recipe to build an environment of type `ROut`, starting from a value `RIn`, and possibly producing an error `E` during creation. --- ## Layer `Layer[+E, +ROut]` is a type alias for `ZLayer[Any, E, ROut]`, which represents a layer that doesn't require any services, it may fail with an error type of `E`, and returns `ROut` as its output. ```scala type Layer[+E, +ROut] = ZLayer[Any, E, ROut] ``` --- ## RLayer `RLayer[-RIn, +ROut]` is a type alias for `ZLayer[RIn, Throwable, ROut]`, which represents a layer that requires `RIn` as its input, it may fail with `Throwable` value, or returns `ROut` as its output. ```scala type RLayer[-RIn, +ROut] = ZLayer[RIn, Throwable, ROut] ``` --- ## TaskLayer `TaskLayer[+ROut]` is a type alias for `ZLayer[Any, Throwable, ROut]`, which represents a layer that doesn't require any services as its input, it may fail with `Throwable` value, and returns `ROut` as its output. ```scala type TaskLayer[+ROut] = ZLayer[Any, Throwable, ROut] ``` --- ## ULayer `ULayer[+ROut]` is a type alias for `ZLayer[Any, Nothing, ROut]`, which represents a layer that doesn't require any services as its input, it can't fail, and returns `ROut` as its output. ```scala type ULayer[+ROut] = ZLayer[Any, Nothing, ROut] ``` --- ## URLayer `URLayer[-RIn, +ROut]` is a type alias for `ZLayer[RIn, Nothing, ROut]`, which represents a layer that requires `RIn` as its input, it can't fail, and returns `ROut` as its output. ```scala type URLayer[-RIn, +ROut] = ZLayer[RIn, Nothing, ROut] ``` --- ## ZEnvironment A `ZEnvironment[R]` is a built-in type-level map for the `ZIO` data type which is responsible for maintaining the environment of a `ZIO` effect. The `ZIO` data type uses this map to maintain all the environmental services and their implementations. For example, assume we have written a `ZEnvironment` containing all built-in services as below: ```scala val environment: ZEnvironment[Console & Clock & Random & System] = ZEnvironment[Console, Clock, Random, System]( Console.ConsoleLive, Clock.ClockLive, Random.RandomLive, System.SystemLive ) ``` This map contains all built-in services and their corresponding implementations. If we evaluate the `ZEnvironment#toString` method, we can see the underlying type-level map something like this. ```scala ZEnvironment( Map( Console -> (zio.Console$ConsoleLive$@76a3e297, 0), Clock -> (zio.Clock$ClockLive$@4d3167f4, 1), Random -> (RandomScala(scala.util.Random$@4eb7f003), 2), System -> (zio.System$SystemLive$@eafc191, 3) ) ) ``` From a ZIO environment point of view, we can think of `ZIO` as the following function: ```scala type ZIO[R, E, A] = ZEnvironment[R] => Either[E, A] or type ZIO[R, E, A] = ZEnvironment[R] => IO[E, A] ``` For example, the `ZIO[Foo & Bar, Throwable, String]` can be thought of as a function from `ZEnvironment[Foo & Bar]` to `Either[Throwable, String]`: :::note The `ZEnvironment` is useful for manually constructing and combining the ZIO environment. So, in most cases, we do not require working directly with this data type. So you can skip reading this page if you are not an advanced user. ::: We can eliminate the environment of `ZIO[R, E, A]` by providing `ZEnvironment[R]` to that effect. Also, we can access the **whole** environment using `ZIO.environment`: ```scala case class AppConfig(poolSize: Int) val myApp: ZIO[AppConfig, IOException, Unit] = ZIO.environment[AppConfig].flatMap { env => val config = env.get[AppConfig] Console.printLine(s"Application started with config: $config") } val eliminated: IO[IOException, Unit] = myApp.provideEnvironment( ZEnvironment(AppConfig(poolSize = 10)) ) ``` :::note In most cases, we do not require using `ZIO.environment` to access the whole environment or the `ZIO#provideEnvironment` to provide effect dependencies. Therefore, most of the time, we use `ZIO.service*` and other `ZIO#provide*` methods to access a specific service from the environment or provide services to a ZIO effect. ::: ## Creation To create an empty ZIO environment: ```scala val empty: ZEnvironment[Any] = ZEnvironment.empty ``` To create a ZIO environment from a simple value: ```scala case class AppConfig(host: String, port: Int) val config: ZEnvironment[AppConfig] = ZEnvironment(AppConfig("localhost", 8080)) ``` ## Operations To **combine** two or multiple environment we can use `union` or `++` operator: ```scala case class AppConfig(host: String, port: Int) val app: ZEnvironment[AppConfig] = ZEnvironment.empty ++ ZEnvironment(AppConfig("localhost", 8080)) ``` To **add** a service to an environment: ```scala case class AppConfig(host: String, port: Int) val app: ZEnvironment[AppConfig] = ZEnvironment.empty.add(AppConfig("localhost", 8080)) ``` To retrieve a service from the environment, we use `get` method: ```scala case class AppConfig(host: String, port: Int) val app: ZEnvironment[AppConfig] = ZEnvironment.empty.add(AppConfig("localhost", 8080)) val appConfig: AppConfig = app.get[AppConfig] ``` ## Providing Multiple Instance of the Same Interface We can express an effect's dependency on multiple services of the type `A` which are keyed by type `K` with `Map[K, A]`. For example, the `ZIO[Map[String, Database], Throwable, Unit]` is an effect that depends on multiple `Database` versions. To access the specified service corresponding to a specific key, we can use the `ZIO.serviceAt[Service](key)` constructor. For example, to access a `Database` service which is specified by the "inmemory" key, we can write: ```scala val database: URIO[Map[String, Database], Option[Database]] = ZIO.serviceAt[Database]("inmemory") ``` A service can be updated at the specified key using the `ZIO#updateServiceAt` operator. ### Multiple Config Example Let's see how we can create a layer comprising multiple instances of `AppConfig`: ```scala case class AppConfig(host: String, port: Int) object AppConfig { val layer: ULayer[Map[String, AppConfig]] = ZLayer.succeedEnvironment( ZEnvironment( Map( "prod" -> AppConfig("production.myapp", 80), "dev" -> AppConfig("development.myapp", 8080) ) ) ) } ``` And here is the application which uses different `AppConfig` from the ZIO environment based on the value of the `APP_ENV` environment variable: ```scala object MultipleConfigExample extends ZIOAppDefault { val myApp: ZIO[Map[String, AppConfig], String, Unit] = for { env <- System.env("APP_ENV") .flatMap(x => ZIO.fromOption(x)) .orElseFail("The environment variable APP_ENV cannot be found.") config <- ZIO.serviceAt[AppConfig](env) .flatMap(x => ZIO.fromOption(x)) .orElseFail(s"The $env config cannot be found in the ZIO environment") _ <- ZIO.logInfo(s"Application started with: $config") } yield () def run = myApp.provide(AppConfig.layer) } ``` ### Multiple Database Example Here is an example of providing multiple instances of the `Database` service to the ZIO environment: ```scala trait Database { def add(key: String, value: Array[Byte]): ZIO[Any, Throwable, Unit] } object Database { val layer: ULayer[Map[String, Database]] = { ZLayer.succeedEnvironment( ZEnvironment( Map( "persistent" -> PersistentDatabase.apply(), "inmemory" -> InmemoryDatabase.apply() ) ) ) } } case class InmemoryDatabase() extends Database { override def add(key: String, value: Array[Byte]): ZIO[Any, Throwable, Unit] = ZIO.unit <* ZIO.logInfo(s"new $key added to the inmemory database") } case class PersistentDatabase() extends Database { override def add(key: String, value: Array[Byte]): ZIO[Any, Throwable, Unit] = ZIO.unit <* ZIO.logInfo(s"new $key added to the persistent database") } object MultipleDatabaseExample extends ZIOAppDefault { val myApp = for { inmemory <- ZIO.serviceAt[Database]("inmemory") .flatMap(x => ZIO.fromOption[Database](x)) .orElseFail("failed to find an in-memory database in the ZIO environment") persistent <- ZIO.serviceAt[Database]("persistent") .flatMap(x => ZIO.fromOption[Database](x)) .orElseFail("failed to find an persistent database in the ZIO environment") _ <- inmemory.add("key1", "value1".getBytes(StandardCharsets.UTF_8)) _ <- persistent.add("key2", "value2".getBytes(StandardCharsets.UTF_8)) } yield () def run = myApp.provideLayer(Database.layer) } ``` --- ## ZIO Environment Use-cases ZIO Environment allows us to describe workflows which carry some context that is used in the course of executing the workflow. This context can be dived into two categories: 1. Local Capabilities, e.g. scopes and transactions 2. Business Logic, e.g. services and repositories Let's discuss each of these in turn. ## Local Capabilities The most idiomatic use of the ZIO environment is for: - Describing as values workflows that use capabilities that are "local" to a particular context - Where tracking the use of this context at the type level is helpful for reasoning about programs Let's look at [`Scope`](../resource/scope.md), which I would say is the most idiomatic usage of the environment in ZIO itself, to see what each of these mean: ### 1. Local Contexts The first criteria is that a capability be "local" to a particular context and not shared throughout the entire application. For example, in the case of `Scope` we typically don't just have one scope for our entire application but many smaller scopes, such as the scope for using a particular file. Usages of local context also have an operator that allows locally eliminating this capability, such as the `ZIO.scoped` operator which transforms a `ZIO[R with Scope, E, A]` to a `ZIO[R, E, A]`. Other potential examples of this would be the [`ZState`](../state-management/zstate.md) data type in ZIO, which describes some local use of state and is eliminated by the `ZIO.stateful` operator, a `Transaction` representing the usage of a database transaction, or the context of a particular HTTP request (`RequestContext`). Notice that using the environment type allows workflows that require these capabilities to be first class values that **compose naturally** and can have their own operators for working with them, which would not be possible if we defined them as methods that required us to explicitly pass around these capabilities. ### 2. Type-level Reasoning The second criteria is that tracking the usage of this context be helpful for reasoning about programs. We can also use a [`FiberRef`](../state-management/fiberref.md) value to maintain some local context. However, when we do so, our usage of that context is not reflected at the type level. For example, when we log something it will use a variety of contextual information including the current log level, the current log span, and the current log annotations but none of this is reflected at the type level: ```scala object ZIO { def log(message: => String)(implicit trace: Trace): ZIO[Any, Nothing, Unit] } ``` Not reflecting this usage of contextual information at the type level can be both an advantage and a disadvantage: - The advantage is that it can create a simpler API because we do not clutter up the environment with additional dependencies. - The disadvantage is that we can't track at the type level whether we are using contextual information or whether we have provided it. In the case of logging this is clearly the right trade-off. Logging is a low level concern that we don't want to require us to update our type signatures, and there is essentially no harm in running a ZIO workflow that does logging without providing this log context since we can just log at some default log level without any log spans or annotations. In contrast, in the case of `Scope` there is tremendous value in reflecting the use of `Scope` at the type level so we know whether a workflow is resourceful and can have operators that reflect at the type level that we have provided a `Scope` to part of our application. Similarly if we have a **database transaction** reflecting at the type level that some workflow needs to be done as part of a transaction and when we are "executing" a transaction is extremely valuable. ## Business Logic The other potential use of the ZIO environment is describing the dependencies of our business logic itself. Normally, we implement higher level services in terms of lower level services using [constructor based dependency injection with ZLayer](../di/index.md). ```scala trait HighLevelService { def doSomething: ZIO[Any, Nothing, Unit] } object HighLevelService { val live: ZLayer[LowLevelService, Nothing, HighLevelService] = ZLayer.fromFunction(HighLevelServiceLive(_)) final case class HighLevelServiceLive(lowLevelService: LowLevelService) extends HighLevelService { def doSomething: ZIO[Any, Nothing, Unit] = ??? // implemented in terms of `LowLevelService` } } trait LowLevelService { def doSomethingElse: ZIO[Any, Nothing, Unit] } object LowLevelService { val live: ZLayer[Any, Nothing, LowLevelService] = ??? } ``` This allows us to avoid using `LowLevelService` in the environment and to not "leak" implementation details, since the dependency on `LowLevelService` is an implementation detail of `HighLevelService` that might not even exist if `HighLevelService` is refactored. However, the question arises then of how we should work with `HighLevelService` in the core of our business logic or the center of the "onion" in the [onion architecture](../architecture/architectural-patterns.md#onion-architecture)? There are two approaches to this. ### Everything as a Service The first approach is just that everything is a service: ```scala sealed trait ApplicationService { def run: ZIO[Any, Nothing, Unit] } object ApplicationService { val live: ZLayer[Any, Nothing, LowLevelService] = ??? final case class ApplicationServiceLive(highLevelService: HighLevelService) extends ApplicationService { val run: ZIO[Any, Nothing, Unit] = ??? // business logic implemented in terms of high level services } } object Main extends ZIOAppDefault { val run = ZIO .serviceWithZIO[ApplicationService](_.run) .provide( ApplicationService.live, HighLevelService.live, LowLevelService.live ) } ``` This style avoids any usage of the ZIO environment that is not a local capability except for possibly a single time within `ZIOAppDefault`. ### Using ZIO Environment There can be a feeling that defining this final `ApplicationLevelService` is unnecessary, and we would like to be able to write our business logic in terms of high level services directly without making it another service: ```scala object Main extends ZIOAppDefault { val myProgramLogic: ZIO[HighLevelService, Nothing, Unit] = for { _ <- ZIO.serviceWithZIO[HighLevelService](_.doSomething) - <- otherLogicHere } yield () val run = myProgramLogic.provide( HighLevelSevice.live, LowLevelService.live ) } ``` There has been some movement towards the "everything is a service" approach since it avoids the need to implement [service accessors](../service-pattern/accessor-methods.md) but it can be a matter of team style which of these approaches to use. Either way our program is the same except for whether in our business logic we call methods on services directly or use the environment for that. To learn more about this approach please see [how we can use dependency injection with the service pattern](../di/dependency-injection-in-zio.md#dependency-injection-and-service-pattern). --- ## ZLayer A `ZLayer[-RIn, +E, +ROut]` describes a layer of an application: every layer in an application requires some services as input `RIn` and produces some services as the output `ROut`. We can think of a layer as mental model of an asynchronous function from `RIn` to the `Either[E, ROut]`: ```scala type ZLayer[-RIn, +E, +ROut] = RIn => async Either[E, ROut] ``` For example, a `ZLayer[Socket & Persistence, Throwable, Database]` can be thought of as a function that map `Socket` and `Persistence` services into `Database` service: ```scala (Socket, Persistence) => Database ``` So we can say that the `Database` service has two dependencies: `Socket` and `Persistence` services. In some cases, a `ZLayer` may not have any dependencies or requirements from the environment. In this case, we can specify `Any` for the `RIn` type parameter. The [`Layer`](layer.md) type alias provided by ZIO is a convenient way to define a layer without requirements. ZLayers are: 1. **Recipes for Creating Services** — They describe how to create services from given dependencies. For example, the `ZLayer[Socket & Database, Throwable, UserRepo]` is a recipe for building a service that requires `Socket` and `Database` service, and it produces a `UserRepo` service. 2. **An Alternative to Constructors** — We can think of `ZLayer` as a more powerful version of a constructor, it is an alternative way to represent a constructor. Like a constructor, it allows us to build the `ROut` service in terms of its dependencies (`RIn`). 3. **Composable** — Because of their excellent **composition properties**, layers are the idiomatic way in ZIO to create services that depend on other services. We can define layers that are relying on each other. 4. **Effectful and Resourceful** — The construction of ZIO layers can be effectful and resourceful. They can be acquired effectfully and safely released when the services are done being utilized or even in case of failure, interruption, or defects in the application. For example, to create a recipe for a `Database` service, we should describe how the `Database` will be initialized using an acquisition action. In addition, it may contain information about how the `Database` releases its connection pools. 6. **Asynchronous** — Unlike class constructors which are blocking, `ZLayer` is fully asynchronous and non-blocking. Note that in non-blocking applications we typically want to avoid creating something that is blocking inside its constructor. For example, when we are constructing some sort of Kafka streaming service, we might want to connect to the Kafka cluster in the constructor of our service, which takes some time. So it wouldn't be a good idea to block inside the constructor. There are some workarounds for fixing this issue, but they are not as perfect as the ZIO solution which allows for asynchronous, non-blocking constructors. 6. **Parallelism** — ZIO layers can be acquired in parallel, unlike class constructors, which do not support parallelism. When we compose multiple layers and then acquire them, the construction of each layer will occur in parallel. This will reduce the initialization time of ZIO applications with a large number of dependencies. With ZIO ZLayer, our constructor could be asynchronous, but it could also block. We can acquire resources asynchronously or in a blocking fashion, and spend some time doing that, and we don't need to worry about it. That is not an anti-pattern. This is the best practice with ZIO. And that is because `ZLayer` has the full power of the `ZIO` data type, and as a result, we have strictly more power on our constructors with `ZLayer`. 7. **Resilient** — Layer construction can be resilient. So if the acquiring phase fails, we can have a schedule to retry the acquiring stage. This helps us write apps that are error-proof and respond appropriately to failures. Let's see how we can create a layer: ## Creation There are four main ways to create a ZLayer: 1. `ZLayer.succeed` for creating layers from simple values. 2. `ZLayer.scoped` for creating layers with _for comprehension_ style from resourceful effects. 3. `ZLayer.apply`/`ZLayer.fromZIO` for creating layers with _for comprehension_ style from effectual but not resourceful effects. 4. `ZLayer.fromFunction` for creating layers that are neither effectual nor resourceful. Now let's look at each of these methods. ### From a Simple Value or an Existing Service With `ZLayer.succeed` we can construct a `ZLayer` from a value. It returns a `ULayer[A]` value, which represents a layer of an application that has a service of type `A`: ```scala def succeed[A: Tag](a: A): ULayer[A] ``` Using `ZLayer.succeed` we can create a layer containing _simple value_ or a _service_: 1. To create a layer from a _simple value_: ```scala case class AppConfig(host: String, port: Int) val configLayer: ULayer[AppConfig] = ZLayer.succeed(AppConfig("localhost", 8080)) ``` In the example above, we created a `configLayer` that provides us an instance of `AppConfig`. 2. To create a layer from an _existing service_: ```scala trait EmailService { def send(email: String, content: String): UIO[Unit] } object EmailService { val layer: ZLayer[Any, Nothing, EmailService] = ZLayer.succeed( new EmailService { override def send(email: String, content: String): UIO[Unit] = ??? } ) } ``` ### From Non-resourceful Effects This is the for-comprehension way of creating a ZIO service using `ZLayer.apply`: ```scala trait A trait B trait C case class CLive(a: A, b: B) extends C object CLive { val layer: ZLayer[A & B, Nothing, C] = ZLayer { for { a <- ZIO.service[A] b <- ZIO.service[B] } yield CLive(a, b) } } ``` ### From Functions A `ZLayer[R, E, A]` can be thought of as a function from `R` to `A`. So we can convert functions to the `ZLayer` using the `ZLayer.fromFunction` constructor. In the following example, the `CLive` implementation requires two `A` and `B` services, and we can easily convert that case class to a `ZLayer`: ```scala trait A trait B trait C case class CLive(a: A, b: B) extends C object CLive { val layer: ZLayer[A & B, Nothing, C] = ZLayer.fromFunction(CLive.apply _) } ``` Below is a complete working example: ```scala case class DatabaseConfig() object DatabaseConfig { val live = ZLayer.succeed(DatabaseConfig()) } case class Database(databaseConfig: DatabaseConfig) object Database { val live: ZLayer[DatabaseConfig, Nothing, Database] = ZLayer.fromFunction(Database.apply _) } case class Analytics() object Analytics { val live: ULayer[Analytics] = ZLayer.succeed(Analytics()) } case class Users(database: Database, analytics: Analytics) object Users { val live = ZLayer.fromFunction(Users.apply _) } case class App(users: Users, analytics: Analytics) { def execute: UIO[Unit] = ZIO.debug(s"This app is made from ${users} and ${analytics}") } object App { val live = ZLayer.fromFunction(App.apply _) } object MainApp extends ZIOAppDefault { def run = ZIO .serviceWithZIO[App](_.execute) .provide( (((DatabaseConfig.live >>> Database.live) ++ Analytics.live >>> Users.live) ++ Analytics.live) >>> App.live ) } ``` ### Automatic Derivation Simple layers can be derived using `ZLayer.derive`. See [Automatic ZLayer Derivation](./automatic-zlayer-derivation.md). ## Converting a Layer to a Scoped Value Every `ZLayer` can be converted to a scoped `ZIO` by using `ZLayer.build`: ```scala trait Database { def close: UIO[Unit] } object Database { def connect: ZIO[Any, Throwable, Database] = ??? } val database: ZLayer[Any, Throwable, Database] = ZLayer.scoped { ZIO.acquireRelease { Database.connect.debug("connecting to the database") } { database => database.close } } val scopedDatabase: ZIO[Scope, Throwable, ZEnvironment[Database]] = database.build ``` ## Falling Back to an Alternate Layer If a layer fails, we can provide an alternative layer by using `ZLayer#orElse` so it will fall back to the second layer: ```scala trait Database val postgresDatabaseLayer: ZLayer[Any, Throwable, Database] = ??? val inmemoryDatabaseLayer: ZLayer[Any, Throwable, Database] = ??? val databaseLayer: ZLayer[Any, Throwable, Database] = postgresDatabaseLayer.orElse(inmemoryDatabaseLayer) ``` ## Converting a Layer to a ZIO Application Sometimes our entire application is a ZIO Layer, e.g. an HTTP Server, so by calling the `ZLayer#launch` we can convert that to a ZIO application. This will build the layer and use it until it is interrupted. ```scala object MainApp extends ZIOAppDefault { val httpServer: ZLayer[Any, Nothing, HttpServer] = ZLayer.make[HttpServer]( JsonParserLive.layer, TemplateEngineLive.layer ) def run = httpServer.launch } ``` ## Retrying We can retry constructing a layer in case of failure: ```scala val databaseLayer: ZLayer[Any, Throwable, DatabaseConnection] = ??? val retriedLayer : ZLayer[Clock, Throwable, DatabaseConnection] = databaseLayer.retry(Schedule.fibonacci(1.second)) ``` ## Layer Projection We can project out a part of `ZLayer` by providing a projection function to the `ZLayer#project` method: ```scala case class Connection(host: String, port: Int) case class Login(user: String, password: String) case class DBConfig( connection: Connection, login: Login ) val connection: ZLayer[DBConfig, Nothing, Connection] = ZLayer.service[DBConfig].project(_.connection) ``` ## Tapping We can perform a specified effect based on the success or failure result of the layer using `ZLayer#tap`/`ZLayer#tapError`. This would not change the layer's signature: ```scala case class AppConfig(host: String, port: Int) val config: ZLayer[Any, Throwable, AppConfig] = ZLayer.fromZIO( ZIO.attempt(???) // reading config from a file ) val res: ZLayer[Any, Throwable, AppConfig] = config .tap(cnf => ZIO.debug(s"layer acquisition succeeded with $cnf")) .tapError(err => ZIO.debug(s"error occurred during reading the config $err")) ``` --- ## Introduction to ZIO's Control Flow Operators Although we have access to built-in Scala control flow structures, ZIO has several control flow combinators. In this section, we are going to introduce different ways of controlling flows in ZIO applications. ## `if` Expression When working with ZIO values, we can use built-in Scala if-then-else expressions: ```scala def validateWeightOption(weight: Double): ZIO[Any, Nothing, Option[Double]] = if (weight >= 0) ZIO.some(weight) else ZIO.none ``` Also, we can encode invalid inputs using the error channel: ```scala def validateWeightOrFail(weight: Double): ZIO[Any, String, Double] = if (weight >= 0) ZIO.succeed(weight) else ZIO.fail(s"negative input: $weight") ``` Even if the input has side effects, we can use `ZIO#flatMap` to access the raw value and write the if-then-else expression: ```scala def validateWeightOrFailZIO[R](weight: ZIO[R, Nothing, Double]): ZIO[R, String, Double] = weight.flatMap { w => if (w >= 0) ZIO.succeed(w) else ZIO.fail(s"negative input: $w") } ``` ## Conditional Operators ### `when` We can also use ZIO's combinators that are the moral equivalent to these expressions: Instead of `if (p) expression` we can use the `ZIO.when` or `ZIO#when` operator: ```scala def validateWeightOption(weight: Double): ZIO[Any, Nothing, Option[Double]] = ZIO.when(weight > 0)(ZIO.succeed(weight)) ``` If the predicate is effectful, we can use `ZIO.whenZIO` or `ZIO#whenZIO` operators. For example, the following function creates a random option of int value: ```scala def randomIntOption: ZIO[Any, Nothing, Option[Int]] = Random.nextInt.whenZIO(Random.nextBoolean) ``` Another nice variant of the `when` operator is `ZIO.whenCase` and also the `ZIO.whenCaseZIO`. Using these operators, we can run an effect when our provided effectful `PartialFunction` matches the given raw or effectful input. The important note regarding this operator is that it is safe, so it will do nothing if the value does not match. Let's try to write a game, which asks users to choose which game to play: ```scala def minesweeper(level: String) = ZIO.attempt(???) def ticTacToe = ZIO.attempt(???) def snake(rows: Int, columns: Int) = ZIO.attempt(???) def myApp = ZIO.whenCaseZIO { (Console.print( "Please choose one game (minesweeper, snake, tictactoe)? " ) *> Console.readLine).orDie } { case "minesweeper" => Console.print( "Please enter the level of the game (easy/hard/medium)?" ) *> Console.readLine.flatMap(minesweeper) case "snake" => Console.printLine( "Please enter the size of the game: " ) *> Console.readLine.mapAttempt(_.toInt).flatMap(n => snake(n, n)) case "tictactoe" => ticTacToe } ``` ### `unless` The `ZIO.unless` and `ZIO#unless` operators are like `when` operators, but they are moral equivalent for the `if (!p) expression` construct. ### `ifZIO` This operator takes an _effectful predicate_, if that predicate is evaluated to true, it will run the `onTrue` effect, otherwise it will run the `onFalse` effect. Let's try to write a simple virtual flip function: ```scala def flipTheCoin: ZIO[Any, IOException, Unit] = ZIO.ifZIO(Random.nextBoolean)( onTrue = Console.printLine("Head"), onFalse = Console.printLine("Tail") ) ``` ## Loop Operators In imperative Scala code bases, sometimes we may use `while(condition) { statement }` or `do { statement } while (condition)` constructs to perform loops: ```scala object MainApp extends scala.App { def printNumbers(from: Int, to: Int): Unit = { var i = from while (i <= to) { println(s"$i") i = i + 1 } } printNumbers(1, 3) } ``` But in functional Scala, we tend to avoid mutable variables. So to have a loop, we would like to use recursion. Let's rewrite the previous example using recursion: ```scala object MainApp extends scala.App { @tailrec def printNumbers(from: Int, to: Int): Unit = { if (from <= to) { println(s"$from") printNumbers(from + 1, to) } else () } printNumbers(1, 3) } // 1 // 2 // 3 ``` In this example, we wrote a recursive function that prints numbers from 1 to 3. While the last effort doesn't use a mutable variable, it's not a pure solution. We have a `println` statement inside our solution, calling this function is not pure so the whole solution is not pure. We know that we can model effectful functions using the ZIO effect system. So let's try rewrite that using ZIO: ```scala object MainApp extends ZIOAppDefault { def printNumbers(from: Int, to: Int): ZIO[Any, IOException, Unit] = { if (from <= to) Console.printLine(s"$from") *> printNumbers(from + 1, to) else ZIO.unit } def run = printNumbers(1, 5) } ``` ZIO provides some loop combinators that help us avoid the need to write explicit recursions. This means that we can do almost anything we want to do without using explicit recursions. Let's rewrite the last solution using `ZIO.loopDiscard`: ```scala object MainApp extends ZIOAppDefault { def printNumbers(from: Int, to: Int): ZIO[Any, IOException, Unit] = { ZIO.loopDiscard(from)(_ <= to, _ + 1)(i => Console.printLine(i)) } def run = printNumbers(1, 3) } ``` After this short introduction to writing loops in functional Scala, now let us go further into ZIO-specific combinators for writing loops: ### `loop` The `ZIO.loop` operator takes an initial state, then repeatedly changes the state based on the given `inc` function, until the given `cont` function evaluates to true: ```scala object ZIO { def loop[R, E, A, S]( initial: => S )(cont: S => Boolean, inc: S => S)(body: S => ZIO[R, E, A]): ZIO[R, E, List[A]] def loopDiscard[R, E, S]( initial: => S )(cont: S => Boolean, inc: S => S)(body: S => ZIO[R, E, Any]): ZIO[R, E, Unit] ``` `ZIO.loop` collects all intermediate states in a list and returns it finally, while the `ZIO.loopDiscard` discards all results. We can think of `ZIO.loop` as a moral equivalent of the following while loop: ```scala var s = initial var as = List.empty[A] while (cont(s)) { as = body(s) :: as s = inc(s) } as.reverse ``` Let's try some examples: ```scala val r1: ZIO[Any, Nothing, List[Int]] = ZIO.loop(1)(_ <= 5, _ + 1)(n => ZIO.succeed(n)).debug // List(1, 2, 3, 4, 5) val r2: ZIO[Any, Nothing, List[Int]] = ZIO.loop(1)(_ <= 5, _ + 1)(n => ZIO.succeed(n * 2)).debug // List(2, 4, 6, 8, 10) val r3: ZIO[Any, IOException, List[Unit]] = ZIO.loop(1)(_ <= 5, _ + 1) { index => Console.printLine(s"Currently at index $index") }.debug // Currently at index 1 // Currently at index 2 // Currently at index 3 // Currently at index 4 // Currently at index 5 // List((), (), (), (), ()) val r4: ZIO[Any, IOException, Unit] = ZIO.loopDiscard(1)(_ <= 5, _ + 1) { index => Console.printLine(s"Currently at index $index") }.debug // Currently at index 1 // Currently at index 2 // Currently at index 3 // Currently at index 4 // Currently at index 5 // () val r5: ZIO[Any, IOException, List[String]] = Console.printLine("Please enter three names: ") *> ZIO.loop(1)(_ <= 3, _ + 1) { n => Console.print(s"$n. ") *> Console.readLine }.debug // Please enter three names: // 1. John // 2. Jane // 3. Joe // List(John, Jane, Joe) ``` ### `iterate` To iterate with the given effectful operation we can use the `ZIO.iterate` combinator. During each iteration, it uses an effectful `body` operation to change the state, and it will continue the iteration while the `cont` function evaluates to true: ```scala object ZIO { def iterate[R, E, S]( initial: => S )(cont: S => Boolean)(body: S => ZIO[R, E, S]): ZIO[R, E, S] } ``` This operator is a moral equivalent of the following while loop: ```scala var s = initial while (cont(s)) { s = body(s) } s ``` Let's try some examples: ```scala val r1 = ZIO.iterate(1)(_ <= 5)(s => ZIO.succeed(s + 1)).debug // 6 val r2 = ZIO.iterate(1)(_ <= 5)(s => ZIO.succeed(s * 2).debug).debug("result") // 2 // 4 // 8 // result: 8 ``` Here's another example. Assume we want to take many names from the user using the terminal. We don't know how many names the user is going to enter. We can ask the user to write "exit" when all inputs are finished. To write such an application, we can use recursion like below: ```scala def getNames: ZIO[Any, IOException, List[String]] = Console.print("Please enter all names") *> Console.printLine(" (enter \"exit\" to indicate end of the list):") *> { def loop( names: List[String] ): ZIO[Any, IOException, List[String]] = { Console.print(s"${names.length + 1}. ") *> Console.readLine .flatMap { case "exit" => ZIO.succeed(names) case name => loop(names.appended(name)) } } loop(List.empty[String]) } // Please enter all names (enter "exit" to indicate end of the list): // 1. John // 2. Jane // 3. Joe // 4. exit // List(John, Jane, Joe) ``` Instead of manually writing recursions, we can rely on well-tested ZIO combinators. So let's rewrite this application using the `ZIO.iterate` operator: ```scala def getNames: ZIO[Any, IOException, List[String]] = Console.print("Please enter all names") *> Console.printLine(" (enter \"exit\" to indicate end of the list):") *> ZIO.iterate((List.empty[String], true))(_._2) { case (names, _) => Console.print(s"${names.length + 1}. ") *> Console.readLine.map { case "exit" => (names, false) case name => (names.appended(name), true) } } .map(_._1) .debug // Please enter all names (enter "exit" to indicate end of the list): // 1. John // 2. Jane // 3. Joe // 4. exit // List(John, Jane, Joe) ``` ### `foreach` Note that, in several cases, we can avoid these low-level operators and instead use high-level ones. For example, let's try to rewrite the `r5` with `ZIO.foreach`: ```scala Console.printLine("Please enter three names:") *> ZIO.foreach(1 to 3) { index => Console.print(s"$index. ") *> Console.readLine }.debug // Please enter three names: // 1. John // 2. Jane // 3. Joe // Vector(John, Jane, Joe) ``` ## try/catch/finally When working with resources, just like Scala's `try`/`catch`/`finally` construct, in ZIO we have a similar operator called `acquireRelease` and also `ensuring`. We discussed them in more detail in the [resource management section](../resource/index.md). But, for now, we want to focus on their control flow behaviors. Let's learn about the `ZIO.acquireReleaseWith` operator. This operator takes three effects: 1. **`acquire`**, an effect that describes the resource acquisition 2. **`release`**, an effect that describes the release of the resource 3. **`use`**, an effect that describes resource usage ```scala ZIO.acquireReleaseWith(acquire = ???)(release = ???)(use = ???) ``` This operator guarantees us that if the _resource acquisition (acquire)_ succeeds, the _release_ effect will be executed whether the _use_ effect succeeded or not: ```scala def wordCount(fileName: String): ZIO[Any, Throwable, Int] = { def openFile(name: => String): ZIO[Any, IOException, Source] = ZIO.attemptBlockingIO(Source.fromFile(name)) def closeFile(source: => Source): ZIO[Any, Nothing, Unit] = ZIO.succeedBlocking(source.close()) def wordCount(source: => Source): ZIO[Any, Throwable, Int] = ZIO.attemptBlocking(source.getLines().length) ZIO.acquireReleaseWith(openFile(fileName))(closeFile(_))(wordCount(_)) } ``` Let's try a simple `acquireRelease` workflow to see how its control flow works: ```scala object MainApp extends ZIOAppDefault { def run = ZIO.acquireReleaseWith { ZIO.succeed("resource").tap(r => ZIO.debug(s"$r acquired")) } { i => ZIO.debug(s"$i released") } { i => ZIO.debug(s"start using $i") } } // Output: // resource acquired // start using resource // resource released ``` --- ## Cause The `ZIO[R, E, A]` effect is polymorphic in values of type `E` and we can work with any error type that we want, but there is a lot of information that is not inside an arbitrary `E` value. So as a result ZIO needs somewhere to store things like **unexpected errors or defects**, **stack and execution traces**, **cause of fiber interruptions**, and so forth. ZIO is very strict about preserving the full information related to a failure. It captures all type of errors into the `Cause` data type. ZIO uses `Cause[E]` to store the full story of failure, so its error model is **lossless**. It doesn't throw away information related to the failure result. So we can figure out exactly what happened during the operation of our effects. It is important to note that `Cause` is the underlying data type for the ZIO data type, and we don't usually deal with it directly. Even though we do not deal with it very often, anytime we want, we can access the `Cause` data structure, which gives us total access to all parallel and sequential errors in our codebase. ## Cause Internals ZIO uses a data structure from functional programming called a _semiring_ for the `Cause` data type. **It allows us to take a base type `E` that represents the error type and then capture the sequential and parallel composition of errors in a fully lossless fashion**. The following snippet shows how `Cause` is designed as a semiring data structure: ```scala sealed abstract class Cause[+E] extends Product with Serializable { self => def trace: Trace = ??? final def ++[E1 >: E](that: Cause[E1]): Cause[E1] = Then(self, that) final def &&[E1 >: E](that: Cause[E1]): Cause[E1] = Both(self, that) } object Cause extends Serializable { case object Empty extends Cause[Nothing] final case class Fail[+E](value: E, override val trace: Trace) extends Cause[E] final case class Die(value: Throwable, override val trace: Trace) extends Cause[Nothing] final case class Interrupt(fiberId: FiberId, override val trace: Trace) extends Cause[Nothing] final case class Stackless[+E](cause: Cause[E], stackless: Boolean) extends Cause[E] final case class Then[+E](left: Cause[E], right: Cause[E]) extends Cause[E] final case class Both[+E](left: Cause[E], right: Cause[E]) extends Cause[E] } ``` Using the `Cause` data structure described above, ZIO can capture all errors inside the application. ## Cause Variations There are several causes for various errors. In this section, we will describe each of these causes. We will see how they can be created manually or how they will be automatically generated as the underlying error management data type of a ZIO application. ### Empty The `Empty` cause indicates the lack of errors. We use `Cause.empty` constructor to create an `Empty` cause. Using `ZIO.failCause` we can create a ZIO effect that has an empty cause: ```scala ZIO.failCause(Cause.empty).cause.debug // Empty ``` Also, we can use `ZIO#cause` to uncover the underlying cause of an effect. For example, we know that `ZIO.succeed(5)` has no errors. So, let's check that: ``` ZIO.succeed(5).cause.debug // Empty ZIO.attempt(5).cause.debug // Empty ``` ### Fail The `Fail` cause indicates the cause of an _expected error_ of type `E`. We can create one using the `Cause.fail` constructor: ```scala ZIO.failCause(Cause.fail("Oh uh!")).cause.debug // Fail(Oh uh!,Trace(Runtime(2,1646395282),Chunk(.MainApp.run(MainApp.scala:4)))) ``` Let's uncover the cause of some ZIO effects especially when we combine them: ```scala ZIO.fail("Oh uh!").cause.debug // Fail(Oh uh!,Trace(Runtime(2,1646395627),Chunk(.MainApp.run(MainApp.scala:3)))) (ZIO.fail("Oh uh!") *> ZIO.dieMessage("Boom!") *> ZIO.interrupt).cause.debug // Fail(Oh uh!,Trace(Runtime(2,1646396370),Chunk(.MainApp.run(MainApp.scala:6)))) (ZIO.fail("Oh uh!") <*> ZIO.fail("Oh Error!")).cause.debug // Fail(Oh uh!,Trace(Runtime(2,1646396419),Chunk(.MainApp.run(MainApp.scala:9)))) val myApp: ZIO[Any, String, Int] = for { i <- ZIO.succeed(5) _ <- ZIO.fail("Oh uh!") _ <- ZIO.dieMessage("Boom!") _ <- ZIO.interrupt } yield i myApp.cause.debug // Fail(Oh uh!,Trace(Runtime(2,1646397126),Chunk(.MainApp.myApp(MainApp.scala:13),.MainApp.run(MainApp.scala:17)))) ``` ### Die The `Die` cause indicates a defect, an _unexpected failure_ of type `Throwable`. It contains the stack trace of the defect that occurred. We can use `Cause.die` to create one: ```scala ZIO.failCause(Cause.die(new Throwable("Boom!"))).cause.debug // Die(java.lang.Throwable: Boom!,Trace(Runtime(2,1646479908),Chunk(.MainApp.run(MainApp.scala:3)))) ``` If we have a bug in our code and something throws an unexpected exception, that information would be described inside a `Die`. Let's try to investigate some ZIO code that will die: ```scala ZIO.succeed(5 / 0).cause.debug // Die(java.lang.ArithmeticException: / by zero,Trace(Runtime(2,1646480112),Chunk(zio.internal.FiberContext.runUntil(FiberContext.scala:538),.MainApp.run(MainApp.scala:3)))) ZIO.dieMessage("Boom!").cause.debug // Stackless(Die(java.lang.RuntimeException: Boom!,Trace(Runtime(2,1646398246),Chunk(.MainApp.run(MainApp.scala:7)))),true) ``` It is worth noting that the latest example is wrapped by the `Stackless` cause in the previous example. We will discuss `Stackless` further, but for now, it is enough to know that `Stackless` includes fewer stack traces than the `Die` cause. ### Interrupt The `Interrupt` cause indicates a fiber interruption which contains information of the _fiber id_ of the interrupted fiber, and also the corresponding stack trace. Let's try an example of: ```scala ZIO.interrupt.cause.debug // Interrupt(Runtime(2,1646471715),Trace(Runtime(2,1646471715),Chunk(.MainApp.run(MainApp.scala:3)))) ZIO.never.fork .flatMap(f => f.interrupt *> f.join) .cause .debug // Interrupt(Runtime(2,1646472025),Trace(Runtime(13,1646472025),Chunk(.MainApp.run(MainApp.scala:7)))) ``` ### Stackless The `Stackless` cause stores stack traces and execution traces. It has a boolean `stackless` flag which denotes whether the ZIO runtime should print the full stack trace of the inner cause or just print a few lines of it. For example, `ZIO.dieMessage` uses `Stackless`: ```scala ZIO.dieMessage("Boom!").cause.debug // Stackless(Die(java.lang.RuntimeException: Boom!,Trace(Runtime(2,1646477970),Chunk(.MainApp.run(MainApp.scala:3)))),true) ``` So when we run it the following stack traces will be printed: ```scala timestamp=2022-03-05T11:08:19.530710679Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.RuntimeException: Boom! at .MainApp.run(MainApp.scala:3)" ``` While `ZIO.die` doesn't use `Stackless` cause: ```scala ZIO.die(new Throwable("Boom!")).cause.debug // Die(java.lang.Exception: Boom!,Trace(Runtime(2,1646479093),Chunk(.MainApp.run(MainApp.scala:3)))) ``` So it prints the full stack trace: ```scala timestamp=2022-03-05T11:19:12.666418357Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.Exception: Boom! at MainApp$.$anonfun$run$1(MainApp.scala:4) at zio.ZIO$.$anonfun$die$1(ZIO.scala:3384) at zio.internal.FiberContext.runUntil(FiberContext.scala:255) at zio.internal.FiberContext.run(FiberContext.scala:115) at zio.internal.ZScheduler$$anon$1.run(ZScheduler.scala:151) at .MainApp.run(MainApp.scala:4)" ``` ### Both When we are doing parallel computation, the effect can fail for more than one reason. If we are doing two things at once and both of them fail then we actually have two errors. So, the `Both` cause stores the composition of two parallel causes. For example, if we run two parallel fibers with `zipPar` and all of them fail, their causes will be encoded with `Both`: ```scala val myApp: ZIO[Any, String, Unit] = for { f1 <- ZIO.fail("Oh uh!").fork f2 <- ZIO.dieMessage("Boom!").fork _ <- (f1 <*> f2).join } yield () myApp.cause.debug // Both(Fail(Oh uh!,Trace(Runtime(13,1646481219),Chunk(.MainApp.myApp(MainApp.scala:5)))),Stackless(Die(java.lang.RuntimeException: Boom!,Trace(Runtime(14,1646481219),Chunk(.MainApp.myApp(MainApp.scala:6)))),true)) ``` If we run the `myApp` effect, in the stack trace we can see two exception traces occurred on two separate fibers: ```scala timestamp=2022-03-05T12:37:46.831096692Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-13" java.lang.String: Oh uh! at .MainApp.myApp(MainApp.scala:5) Exception in thread "zio-fiber-14" java.lang.RuntimeException: Boom! at .MainApp.myApp(MainApp.scala:6)" ``` Other parallel operators are also the same, for example, ZIO encodes the underlying cause of `(ZIO.fail("Oh uh!") <&> ZIO.dieMessage("Boom!"))` with the `Both` cause. ### Then ZIO uses `Then` cause to encode sequential errors. For example, if we perform ZIO's analog of `try-finally` (e.g. `ZIO#ensuring`), and both `try` and `finally` blocks fail, their causes are encoded with `Then`: ```scala val myApp = ZIO.fail("first") .ensuring(ZIO.die(throw new Exception("second"))) myApp.cause.debug // Then(Fail(first,Trace(Runtime(2,1646486975),Chunk(.MainApp.myApp(MainApp.scala:4),.MainApp.myApp(MainApp.scala:5),.MainApp.run(MainApp.scala:7)))),Die(java.lang.Exception: second,Trace(Runtime(2,1646486975),Chunk(zio.internal.FiberContext.runUntil(FiberContext.scala:538),.MainApp.myApp(MainApp.scala:5),.MainApp.run(MainApp.scala:7))))) ``` If we run the `myApp` effect, we can see the following stack trace: ```scala timestamp=2022-03-05T13:30:17.335173071Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.String: first at .MainApp.myApp(MainApp.scala:4) at .MainApp.myApp(MainApp.scala:5) Suppressed: java.lang.Exception: second at MainApp$.$anonfun$myApp$3(MainApp.scala:5) at zio.ZIO$.$anonfun$die$1(ZIO.scala:3384) at zio.internal.FiberContext.runUntil(FiberContext.scala:255) at zio.internal.FiberContext.run(FiberContext.scala:115) at zio.internal.ZScheduler$$anon$1.run(ZScheduler.scala:151) at zio.internal.FiberContext.runUntil(FiberContext.scala:538) at .MainApp.myApp(MainApp.scala:5)" ``` As we can see in the above stack trace, the _first_ failure was suppressed by the _second_ defect. --- ## Exit An `Exit[E, A]` value describes how fibers end life. It has two possible values: - `Exit.Success` contain a success value of type `A`. - `Exit.Failure` contains a failure [Cause](cause.md) of type `E`. This is how the `Exit` data type is defined: ```scala sealed abstract class Exit[+E, +A] extends Product with Serializable { self => // Exit operators } object Exit { final case class Success[+A](value: A) extends Exit[Nothing, A] final case class Failure[+E](cause: Cause[E]) extends Exit[E, Nothing] } ``` We can call `ZIO#exit` on our effect to determine the Success or Failure of our fiber: ```scala val result: ZIO[Any, IOException, Unit] = for { successExit <- ZIO.succeed(1).exit _ <- successExit match { case Exit.Success(value) => printLine(s"exited with success value: ${value}") case Exit.Failure(cause) => printLine(s"exited with failure state: $cause") } } yield () ``` ## Pre-constructed Exit Values ZIO provides several pre-constructed `Exit` values for common cases: - `Exit.unit` — A success exit with a `Unit` value - `Exit.none` — A success exit with a `None` value (type: `Exit[Nothing, Option[Nothing]]`) These values are useful when you need to return a pre-made exit without constructing it manually: ```scala // Using Exit.unit for effects that only care about success or failure val unitExit: Exit[String, Unit] = Exit.unit // Using Exit.none for optional values val noneExit: Exit[String, Option[Nothing]] = Exit.none ``` --- ## Core Data Types In this section we are going to talk about the basic data types that are required to build a ZIO application: - **[ZIO](zio/zio.md)** — `ZIO` is a value that models an effectful program, which might fail or succeed. + **[UIO](zio/uio.md)** — `UIO[A]` is a type alias for `ZIO[Any, Nothing, A]`. + **[URIO](zio/urio.md)** — `URIO[R, A]` is a type alias for `ZIO[R, Nothing, A]`. + **[Task](zio/task.md)** — `Task[A]` is a type alias for `ZIO[Any, Throwable, A]`. + **[RIO](zio/rio.md)** — `RIO[R, A]` is a type alias for `ZIO[R, Throwable, A]`. + **[IO](zio/io.md)** — `IO[E, A]` is a type alias for `ZIO[Any, E, A]`. - **[ZIOApp](zioapp.md)** — `ZIOApp` and the `ZIOAppDefault` are entry points for ZIO applications. - **[Runtime](runtime.md)** — `Runtime[R]` is capable of executing tasks within an environment `R`. - **[Exit](exit.md)** — `Exit[E, A]` describes the result of executing an `IO` value. - **[Cause](cause.md)** — `Cause[E]` is a description of a full story of a fiber failure. --- ## Runtime A `Runtime[R]` is capable of executing tasks within an environment `R`. To run an effect, we need a `Runtime`, which is capable of executing effects. Runtimes bundle a thread pool together with the environment that effects need. ## What is a Runtime System? Whenever we write a ZIO program, we create a ZIO effect from ZIO constructors plus using its combinators. We are building a blueprint. A ZIO effect is just a data structure that describes the execution of a concurrent program. So we end up with a tree data structure that contains lots of different data structures combined together to describe what the ZIO effect should do. This data structure doesn't do anything, it is just a description of a concurrent program. So the most important thing we should keep in mind when we are working with a functional effect system like ZIO is that when we are writing code, printing a string onto the console, reading a file, querying a database, and so forth, we are just writing a workflow or blueprint of an application. We are just building a data structure. So how can ZIO run these workflows? This is where the ZIO Runtime System comes into play. Whenever we run an `unsafe.run` function, the Runtime System is responsible for stepping through all the instructions described by the ZIO effect and executing them. To simplify everything, we can think of a Runtime System like a black box that takes both the ZIO effect (`ZIO[R, E, A]`) and its environment (`R`). It will run this effect and return its result as an `Either[E, A]` value. ![ZIO Runtime System](/img/zio-runtime-system.svg) ## Responsibilities of the Runtime System Runtime Systems have a lot of responsibilities: 1. **Execute every step of the blueprint** — They have to execute every step of the blueprint in a while loop until it's done. 2. **Handle unexpected errors** — They have to handle unexpected errors, not just the expected ones but also the unexpected ones. 3. **Spawn concurrent fibers** — They are actually responsible for the concurrency that effect systems have. They have to spawn a new fiber every time we call `fork` on an effect. 4. **Cooperatively yield to other fibers** — They have to cooperatively yield to other fibers so that fibers that are sort of hogging the spotlight, don't get to monopolize all the CPU resources. They have to make sure that the fibers split the CPU cores among all the fibers that are working. 5. **Capture execution and stack traces** — They have to keep track of where we are in the progress of our own user-land code, so detailed execution traces can be captured. 6. **Ensure finalizers are run appropriately** — They have to ensure finalizers are run appropriately at the right point in all circumstances to make sure that resources are closed and clean-up logic is executed. This is the feature that powers `Scope` and all the other resource-safe constructs in ZIO. 7. **Handle asynchronous callbacks** — They have to handle this messy job of dealing with asynchronous callbacks. So we don't have to deal with async code. When we are using ZIO, everything is just async out of the box. ## Running a ZIO Effect There are two common ways to run a ZIO effect. Most of the time, we use the [`ZIOAppDefault`](zioapp.md) trait. There are, however, some advanced use cases for which we need to directly feed a ZIO effect into the runtime system's `unsafe.run` method: ```scala object RunZIOEffectUsingUnsafeRun extends scala.App { val myAppLogic = for { _ <- Console.printLine("Hello! What is your name?") n <- Console.readLine _ <- Console.printLine("Hello, " + n + ", good to meet you!") } yield () Unsafe.unsafe { implicit unsafe => zio.Runtime.default.unsafe.run( myAppLogic ).getOrThrowFiberFailure() } } ``` We don't usually use this method to run our effects. One of the use cases of this method is when we are integrating legacy (non-effectful) code with the ZIO effect. It helps us to refactor a large legacy code base into a ZIO effect gradually: assume we have decided to refactor a component in the middle of an application and rewrite that with ZIO. We can start rewriting that component with the ZIO effect and then integrate that component with the existing code base using the `unsafe.run` function. ## Default Runtime ZIO contains a default runtime called `Runtime.default` designed to work well for mainstream usage. It is implemented as below: ```scala object Runtime { val default: Runtime[Any] = Runtime(ZEnvironment.empty, FiberRefs.empty, RuntimeFlags.default) } ``` The default runtime provides the minimum capabilities to bootstrap execution of ZIO tasks. We can easily access the default `Runtime` to run an effect: ```scala object MainApp extends scala.App { val myAppLogic = ZIO.succeed(???) val runtime = Runtime.default Unsafe.unsafe { implicit unsafe => runtime.unsafe.run(myAppLogic).getOrThrowFiberFailure() } } ``` ## Top-level and Locally Scoped Runtimes In ZIO, we have two types of runtimes: - **Top-level runtime** is the one that is used to run the entire ZIO application from the very beginning. There is only one top-level runtime when running a ZIO application. Here are some use-cases: - Creating a top level runtime in a mixed application. For example, if we are using an HTTP library that does not have direct support for ZIO we may need to use `Runtime.unsafe.run` in the implementations of each of our routes. - Another use-case is when we want to install a custom monitoring or supervisor from the very beginning of the application. - **Locally scoped runtimes** are used during the execution of the ZIO application. They are local to a specific region of the code. Suppose we want to change the runtime configurations in the middle of a ZIO application. In such cases, we use locally scoped runtimes, for example: - When we want to import an effectful or side-effecting application with a specific runtime. - In some performance-critical regions, we want to disable logging temporarily. - When we want to have a customized executor for running a portion of our code. ZLayer provides a consistent way to customize and configure runtimes. Using layers to customize the runtime enables us to use ZIO workflows. So a configuration workflow can be pure, effectful, or resourceful. Let's say we want to customize the runtime based on configuration information from a file or database. In most cases, it is sufficient to customize application runtime using the [`bootstrap` layer](#configuring-runtime-using-bootstrap-layer) or [providing a custom configuration](#configuring-runtime-by-providing-configuration-layers) directly to our application. If none of these solutions fit to our problem, we can use [top-level runtime configurations](#top-level-runtime-configuration). Let's talk about each solution in detail. ## Locally Scoped Runtime Configuration In ZIO all runtime configurations are inherited from their parent workflows. So whenever we access a runtime configuration, or obtain a runtime inside a workflow, we are accessing the runtime of the parent workflow. We can override the runtime configuration of the parent workflow by providing a new configuration to a region of the code. This is called locally scoped runtime configuration. When the execution of that region is finished, the runtime configuration will be restored to its original value. We mainly use `ZIO#provideXYZ` operators to provide a new runtime configuration to a specific region of the code: ### Configuring Runtime by Providing Configuration Layers By providing (`ZIO#provideXYZ`) runtime configuration layers to a ZIO workflow, we can change the runtime configs easily: ```scala object MainApp extends ZIOAppDefault { val addSimpleLogger: ZLayer[Any, Nothing, Unit] = Runtime.addLogger((_, _, _, message: () => Any, _, _, _, _) => println(message())) def run = { for { _ <- ZIO.log("Application started!") _ <- ZIO.log("Application is about to exit!") } yield () }.provide(Runtime.removeDefaultLoggers ++ addSimpleLogger) } ``` The output: ```scala Application started! Application is about to exit! ``` To provide runtime configuration to a specific region of a ZIO application, we should provide the configuration layer only to that specific region: ```scala object MainApp extends ZIOAppDefault { val addSimpleLogger: ZLayer[Any, Nothing, Unit] = Runtime.addLogger((_, _, _, message: () => Any, _, _, _, _) => println(message())) def run = for { _ <- ZIO.log("Application started!") _ <- { for { _ <- ZIO.log("I'm not going to be logged!") _ <- ZIO.log("I will be logged by the simple logger.").provide(addSimpleLogger) _ <- ZIO.log("Reset back to the previous configuration, so I won't be logged.") } yield () }.provide(Runtime.removeDefaultLoggers) _ <- ZIO.log("Application is about to exit!") } yield () } ``` The output: ```scala timestamp=2022-08-31T14:28:34.711461Z level=INFO thread=#zio-fiber-6 message="Application started!" location=.MainApp.run file=ZIOApp.scala line=9 I will be logged by the simple logger. timestamp=2022-08-31T14:28:34.832035Z level=INFO thread=#zio-fiber-6 message="Application is about to exit!" location=.MainApp.run file=ZIOApp.scala line=17 ``` ### Configuring Runtime Using `bootstrap` Layer The `bootstrap` layer is a special layer that is mainly used to acquire and release services that are necessary for the application to run. However, it can also be applied to runtime customization as well. This solution requires us to override the `bootstrap` layer from the `ZIOApp` trait. By using this technique, after initialization of the top-level runtime, it will provide the `bootstrap` layer to the ZIO application given through the `run` method. ```scala object MainApp extends ZIOAppDefault { val addSimpleLogger: ZLayer[Any, Nothing, Unit] = Runtime.addLogger((_, _, _, message: () => Any, _, _, _, _) => println(message())) override val bootstrap: ZLayer[Any, Nothing, Unit] = Runtime.removeDefaultLoggers ++ addSimpleLogger def run = for { _ <- ZIO.log("Application started!") _ <- ZIO.log("Application is about to exit!") } yield () } ``` The output: ```scala Application started! Application is about to exit! ``` Although using this method will apply the configuration layer to the whole ZIO application, it is categorized as local runtime configuration because the `bootstrap` layer is evaluated and applied after the top-level runtime is initialized. So it will only be applied to the ZIO application given through the `run` method. To elaborate more on this, let's look at the following example: ```scala object MainApp extends ZIOAppDefault { val addSimpleLogger: ZLayer[Any, Nothing, Unit] = Runtime.addLogger((_, _, _, message: () => Any, _, _, _, _) => println(message())) val effectfulConfiguration: ZLayer[Any, Nothing, Unit] = ZLayer.fromZIO(ZIO.log("Started effectful workflow to customize runtime configuration")) override val bootstrap: ZLayer[Any, Nothing, Unit] = Runtime.removeDefaultLoggers ++ addSimpleLogger ++ effectfulConfiguration def run = for { _ <- ZIO.log("Application started!") _ <- ZIO.log("Application is about to exit!") } yield () } ``` What do we expect to see as the output? We have `Runtime.removeDefaultLoggers` which removes the default logger from the runtime. So we expect to see log messages only from the simple logger. But that is not the case. We have an effectful configuration layer that is evaluated after the top-level runtime is initialized. So we can see the log message related to the initialization of `effectfulConfiguration` layer from the default logger: ```scala timestamp=2022-09-01T08:07:47.870219Z level=INFO thread=#zio-fiber-6 message="Started effectful workflow to customize runtime configuration" location=.MainApp.effectfulConfiguration file=ZIOApp.scala line=8 Application started! Application is about to exit! ``` ### Enabling Virtual Threads It is possible to configure the Runtime to use Java virtual threads if you are using JDK 21 and later. ZIO offers two ways to utilize virtual threads: 1. For the main executor (handles non-blocking ZIO operations): ```scala object MainApp extends ZIOAppDefault { override val bootstrap = Runtime.enableLoomBasedExecutor override def run = ZIO.attempt { println(s"Task running on a virtual-thread: ${Thread.currentThread().getName()}") } } ``` 2. For the blocking executor (handles blocking operations): ```scala object MainApp extends ZIOAppDefault { override val bootstrap = Runtime.enableLoomBasedBlockingExecutor override def run = ZIO.attemptBlocking { println(s"Blocking task running on a virtual-thread: ${Thread.currentThread().getName()}") } } ``` ## Top-level Runtime Configuration When we write a ZIO application using the `ZIOAppDefault` trait, a default top-level runtime is created and used to run the application automatically under the hood. Further, we can customize the rest of the ZIO application by providing locally scoped configuration layers using [`provideXYZ` operations](#configuring-runtime-by-providing-configuration-layers) or [`bootstrap` layer](#configuring-runtime-using-bootstrap-layer). This is usually sufficient for lots of ZIO applications, but it is not always the case. There are cases where we want to customize the runtime of the entire ZIO application from the top level. In such cases, we need to create a top-level runtime by unsafely running the configuration layer to convert that configuration to the `Runtime` by using the `Runtime.unsafe.fromLayer` operator: ```scala val runtime: Runtime[Any] = Unsafe.unsafe { implicit unsafe => Runtime.unsafe.fromLayer(layer) } ``` Let's try a fully working example: ```scala object MainApp extends ZIOAppDefault { // In a real-world application we might need to implement a `sl4jlogger` layer val addSimpleLogger: ZLayer[Any, Nothing, Unit] = Runtime.addLogger((_, _, _, message: () => Any, _, _, _, _) => println(message())) val layer: ZLayer[Any, Nothing, Unit] = Runtime.removeDefaultLoggers ++ addSimpleLogger override val runtime: Runtime[Any] = Unsafe.unsafe { implicit unsafe => Runtime.unsafe.fromLayer(layer) } def run = ZIO.log("Application started!") } ``` :::caution Keep in mind that only the "bootstrap" layer of applications will be combined when we compose two ZIO applications. Therefore, when we compose two ZIO programs, top-level runtime configurations won't be integrated. ::: Another use-case of top-level runtimes is when we want to integrate our ZIO application inside a legacy application: ```scala object MainApp { val sl4jlogger: ZLogger[String, Any] = ??? def legacyApplication(input: Int): Unit = ??? val zioWorkflow: ZIO[Any, Nothing, Int] = ??? val runtime: Runtime[Unit] = Unsafe.unsafe { implicit unsafe => Runtime.unsafe .fromLayer( Runtime.removeDefaultLoggers ++ Runtime.addLogger(sl4jlogger) ) } def zioApplication(): Int = Unsafe.unsafe { implicit unsafe => runtime.unsafe .run(zioWorkflow) .getOrThrowFiberFailure() } def main(args: Array[String]): Unit = { val result = zioApplication() legacyApplication(result) } } ``` ## Providing Environment to Runtime System The custom runtime can be used to run many different effects that all require the same environment, so we don't have to call `ZIO#provide` on all of them before we run them. For example, assume we want to create a `Runtime` for services that are for testing purposes, and they don't interact with real external APIs. So we can create a runtime especially for testing. Let's say we have defined two `LoggingService` and `EmailService` services: ```scala trait LoggingService { def log(line: String): UIO[Unit] } object LoggingService { def log(line: String): URIO[LoggingService, Unit] = ZIO.serviceWithZIO[LoggingService](_.log(line)) } trait EmailService { def send(user: String, content: String): Task[Unit] } object EmailService { def send(user: String, content: String): ZIO[EmailService, Throwable, Unit] = ZIO.serviceWithZIO[EmailService](_.send(user, content)) } ``` We are going to implement a live version of `LoggingService` and also a fake version of `EmailService` for testing: ```scala case class LoggingServiceLive() extends LoggingService { override def log(line: String): UIO[Unit] = ZIO.succeed(print(line)) } case class EmailServiceFake() extends EmailService { override def send(user: String, content: String): Task[Unit] = ZIO.attempt(println(s"sending email to $user")) } ``` Let's create a custom runtime that contains these two service implementations in its environment: ```scala val testableRuntime = Runtime( ZEnvironment[LoggingService, EmailService](LoggingServiceLive(), EmailServiceFake()), FiberRefs.empty, RuntimeFlags.default ) ``` Also, we can replace the environment of the default runtime with our own custom environment, which allows us to add new services to the ZIO environment: ```scala val testableRuntime: Runtime[LoggingService with EmailService] = Runtime.default.withEnvironment { ZEnvironment[LoggingService, EmailService](LoggingServiceLive(), EmailServiceFake()) } ``` Now we can run our effects using this custom `Runtime`: ```scala Unsafe.unsafe { implicit unsafe => testableRuntime.unsafe.run( for { _ <- LoggingService.log("sending newsletter") _ <- EmailService.send("David", "Hi! Here is today's newsletter.") } yield () ).getOrThrowFiberFailure() } ``` --- ## IO `IO[E, A]` is a type alias for `ZIO[Any, E, A]`, which represents an effect that has no requirements, and may fail with an `E`, or succeed with an `A`. :::note In Scala, the _type alias_ is a way to give a name to another type, to avoid having to repeat the original type again and again. It doesn't affect the type-checking process. It just helps us to have an expressive API design. ::: Let's see how the `IO` type alias is defined: ```scala type IO[+E, +A] = ZIO[Any, E, A] ``` So `IO` is equal to a `ZIO` that doesn't need any requirement. `ZIO` values of type `IO[E, Nothing]` (where the value type is `Nothing`) are considered _unproductive_, because the `Nothing` type is _uninhabitable_, i.e. there can be no actual values of type `Nothing`. Values of this type may fail with an `E`, but will never produce a value. :::note Principle of Least Power The `ZIO` data type is the most powerful effect in the ZIO library. It helps us to model various types of workflows. On the other hand, the type aliases are a way of specializing the `ZIO` type for less powerful workflows. Often, we don't need such a piece of powerful machinery. So as a rule of thumb, whenever we require a less powerful effect, it's better to use the appropriate specialized type alias. So there is no need to convert type aliases to the `ZIO` data type, and whenever the `ZIO` data type is required, we can use the most precise type alias to fit our workflow requirement. ::: --- ## RIO `RIO[R, A]` is a type alias for `ZIO[R, Throwable, A]`, which represents an effect that requires an `R`, and may fail with a `Throwable` value, or succeed with an `A`. :::note In Scala, the _type alias_ is a way to give a name to another type, to avoid having to repeat the original type again and again. It doesn't affect the type-checking process. It just helps us to have an expressive API design. ::: Let's see how `RIO` is defined: ```scala type RIO[-R, +A] = ZIO[R, Throwable, A] ``` So `RIO` is equal to a `ZIO` that requires `R`, and whose error channel is `Throwable`. It succeeds with `A`. :::note _Principle of Least Power_ The `ZIO` data type is the most powerful effect in the ZIO library. It helps us to model various types of workflows. On the other hand, the type aliases are a way of specializing the `ZIO` type for less powerful workflows. Often, we don't need such a piece of powerful machinery. So as a rule of thumb, whenever we require a less powerful effect, it's better to use the appropriate specialized type alias. So there is no need to convert type aliases to the `ZIO` data type, and whenever the `ZIO` data type is required, we can use the most precise type alias to fit our workflow requirement. ::: --- ## Task `Task[A]` is a type alias for `ZIO[Any, Throwable, A]`, which represents an effect that has no requirements, and may fail with a `Throwable` value, or succeed with an `A`. :::note In Scala, a _type alias_ is a way to give a name to another type, to avoid having to repeat the original type again and again. It doesn't affect results of the type-checking process. It just helps us to have an expressive API design. ::: Let's see how the `Task` type alias is defined: ```scala type Task[+A] = ZIO[Any, Throwable, A] ``` So a `Task` is equivalent to a `ZIO` that doesn't need any requirement, and may fail with a `Throwable`, or succeed with an `A` value. Sometimes we know that our effect may fail, but we don't care about the type of that exception. This is where we can use `Task`. The signature of this type alias is similar to `Future[T]` and Cats `IO`. If we want to be less precise and eliminate the need to think about requirements and error types, we can use `Task`. This type alias is a good starting point for anyone who wants to refactor an existing code base which is written with Cats `IO` or Monix `Task`. :::note Principle of Least Power The `ZIO` data type is the most powerful effect in the ZIO library. It helps us to model various types of workflows. On the other hand, the type aliases are a way of specializing the `ZIO` type for less powerful workflows. Often, we don't need such a piece of powerful machinery. So as a rule of thumb, whenever we require a less powerful effect, it's better to use the appropriate specialized type alias. So there is no need to convert type aliases to the `ZIO` data type, and whenever the `ZIO` data type is required, we can use the most precise type alias to fit our workflow requirement. ::: --- ## UIO `UIO[A]` is a type alias for `ZIO[Any, Nothing, A]`, which represents an **Unexceptional** effect that doesn't require any specific environment, and cannot fail, but can succeed with an `A`. :::note In Scala, the _type alias_ is a way to give a name to another type, to avoid having to repeat the original type again and again. It doesn't affect the type-checking process. It just helps us to have an expressive API design. ::: Let's see how the `UIO` type alias is defined: ```scala type UIO[+A] = ZIO[Any, Nothing, A] ``` So `UIO` is equal to a `ZIO` that doesn't need any requirement (because it accepts `Any` environment) and that cannot fail (because in Scala the `Nothing` type is _uninhabitable_, i.e. there can be no actual value of type `Nothing`). It succeeds with `A`. `ZIO` values of type `UIO[A]` are considered _infallible_. Values of this type may produce an `A`, but will never fail. Let's write a Fibonacci function. In the following example, the `fib` function is an unexceptional effect, since it has no requirements, we don't expect any failure, and it succeeds with a value of type `Int`: ```scala def fib(n: Int): UIO[Int] = if (n <= 1) { ZIO.succeed(1) } else { for { fiber1 <- fib(n - 2).fork fiber2 <- fib(n - 1).fork v2 <- fiber2.join v1 <- fiber1.join } yield v1 + v2 } ``` :::note Principle of Least Power The `ZIO` data type is the most powerful effect in the ZIO library. It helps us to model various types of workflows. On the other hand, the type aliases are a way of specializing the `ZIO` type for less powerful workflows. Often, we don't need such a piece of powerful machinery. So as a rule of thumb, whenever we require a less powerful effect, it's better to use the appropriate specialized type alias. So there is no need to convert type aliases to the `ZIO` data type, and whenever the `ZIO` data type is required, we can use the most precise type alias to fit our workflow requirement. ::: --- ## URIO `URIO[R, A]` is a type alias for `ZIO[R, Nothing, A]`, which represents an effect that requires an `R`, and cannot fail, but can succeed with an `A`. :::note In Scala, the _type alias_ is a way to give a name to another type, to avoid having to repeat the original type again and again. It doesn't affect the type-checking process. It just helps us to have an expressive API design. ::: Let's see how the `URIO` type alias is defined: ```scala type URIO[-R, +A] = ZIO[R, Nothing, A] ``` So `URIO` is equal to a `ZIO` that requires `R` and cannot fail (because in Scala the `Nothing` type has no inhabitant, so we can't create an instance of type `Nothing`). It succeeds with `A`. :::note Principle of Least Power The `ZIO` data type is the most powerful effect in the ZIO library. It helps us to model various types of workflows. On the other hand, the type aliases are a way of specializing the `ZIO` type for less powerful workflows. Often, we don't need such a piece of powerful machinery. So as a rule of thumb, whenever we require a less powerful effect, it's better to use the appropriate specialized type alias. So there is no need to convert type aliases to the `ZIO` data type, and whenever the `ZIO` data type is required, we can use the most precise type alias to fit our workflow requirement. ::: --- ## ZIO A `ZIO[R, E, A]` value is an immutable value that lazily describes a workflow or job. The workflow requires some environment `R`, and may fail with an error of type `E`, or succeed with a value of type `A`. A value of type `ZIO[R, E, A]` is like an effectful version of the following function type: ```scala R => Either[E, A] ``` This function, which requires an `R`, might produce either an `E`, representing failure, or an `A`, representing success. ZIO effects are not actually functions, of course, they can model synchronous, asynchronous, concurrent, parallel, and resourceful computations. ZIO effects use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability. The `ZIO[R, E, A]` data type has three type parameters: - **`R` - Environment Type**. The effect requires an environment of type `R`. If this type parameter is `Any`, it means the effect has no requirements, because we can run the effect with any value (for example, the unit value `()`). - **`E` - Failure Type**. The effect may fail with a value of type `E`. Some applications will use `Throwable`. If this type parameter is `Nothing`, it means the effect cannot fail, because there are no values of type `Nothing`. - **`A` - Success Type**. The effect may succeed with a value of type `A`. If this type parameter is `Unit`, it means the effect produces no useful information, while if it is `Nothing`, it means the effect runs forever (or until failure). In the following example, the `readLine` function does not require any services, it may fail with value of type `IOException`, or may succeed with a value of type `String`: ```scala val readLine: ZIO[Any, IOException, String] = Console.readLine ``` `ZIO` values are immutable, and all `ZIO` functions produce new `ZIO` values, enabling `ZIO` to be reasoned about and used like any ordinary Scala immutable data structure. `ZIO` values do not actually _do_ anything; they are just values that _model_ or _describe_ effectful interactions. `ZIO` can be _interpreted_ by the ZIO runtime system into effectful interactions with the external world. Ideally, this occurs at a single time, in our application's `main` function. The `App` class provides this functionality automatically. ## Creation In this section we explore some of the common ways to create ZIO effects from values, from common Scala types, and from both synchronous and asynchronous side-effects. Here is the summary list of them: ### Success Values Using the `ZIO.succeed` method, we can create an effect that succeeds with the specified value: ```scala val s1 = ZIO.succeed(42) ``` ### Failure Values Using the `ZIO.fail` method, we can create an effect that models failure: ```scala val f1 = ZIO.fail("Uh oh!") ``` For the `ZIO` data type, there is no restriction on the error type. We may use strings, exceptions, or custom data types appropriate for our application. Many applications will model failures with classes that extend `Throwable` or `Exception`: ```scala val f2 = ZIO.fail(new Exception("Uh oh!")) ``` ### From Values ZIO contains several constructors which help us to convert various data types into `ZIO` effects. #### Option 1. **`ZIO.fromOption`**— An `Option` can be converted into a ZIO effect using `ZIO.fromOption`: ```scala val zoption: IO[Option[Nothing], Int] = ZIO.fromOption(Some(2)) ``` The error type of the resulting effect is `Option[Nothing]`, which provides no information on why the value is not there. We can change the `Option[Nothing]` into a more specific error type using `ZIO#mapError`: ```scala val zoption2: IO[String, Int] = zoption.mapError(_ => "It wasn't there!") ``` We can also readily compose it with other operators while preserving the optional nature of the result (similar to an `OptionT`): ```scala val maybeId: IO[Option[Nothing], String] = ZIO.fromOption(Some("abc123")) def getUser(userId: String): IO[Throwable, Option[User]] = ??? def getTeam(teamId: String): IO[Throwable, Team] = ??? val result: IO[Throwable, Option[(User, Team)]] = (for { id <- maybeId user <- getUser(id).some team <- getTeam(user.teamId).asSomeError } yield (user, team)).unsome ``` 2. **`ZIO.some`**/**`ZIO.none`**— These constructors can be used to directly create ZIO of optional values: ```scala val someInt: ZIO[Any, Nothing, Option[Int]] = ZIO.some(3) val noneInt: ZIO[Any, Nothing, Option[Nothing]] = ZIO.none ``` 3. **`ZIO.getOrFail`**— We can lift an `Option` into a `ZIO` and if the option is not defined we can fail the ZIO with the proper error type: - `ZIO.getOrFail` fails with `Throwable` error type. - `ZIO.getOrFailUnit` fails with `Unit` error type. - `ZIO.getOrFailWith` fails with custom error type. ```scala def parseInt(input: String): Option[Int] = input.toIntOption // If the optional value is not defined it fails with Throwable error type: val r1: ZIO[Any, Throwable, Int] = ZIO.getOrFail(parseInt("1.2")) // If the optional value is not defined it fails with Unit error type: val r2: ZIO[Any, Unit, Int] = ZIO.getOrFailUnit(parseInt("1.2")) // If the optional value is not defined it fail with given error type: val r3: ZIO[Any, NumberFormatException, Int] = ZIO.getOrFailWith(new NumberFormatException("invalid input"))(parseInt("1.2")) ``` 4. **`ZIO.noneOrFail`**— It lifts an option into a ZIO value. If the option is empty it succeeds with `Unit` and if the option is defined it fails with a proper error type: - `ZIO.noneOrFail` fails with the content of the optional value. - `ZIO.noneOrFailUnit` fails with the `Unit` error type. - `ZIO.noneOrFailWith` fails with custom error type. ```scala val optionalValue: Option[String] = ??? // If the optional value is empty it succeeds with Unit // If the optional value is defined it will fail with the content of the optional value val r1: ZIO[Any, String, Unit] = ZIO.noneOrFail(optionalValue) // If the optional value is empty it succeeds with Unit // If the optional value is defined, it will fail by applying the error function to it: val r2: ZIO[Any, NumberFormatException, Unit] = ZIO.noneOrFailWith(optionalValue)(e => new NumberFormatException(e)) ``` #### Either | Function | Input Type | Output Type | |--------------|----------------|---------------------------| | `fromEither` | `Either[E, A]` | `IO[E, A]` | | `left` | `A` | `UIO[Either[A, Nothing]]` | | `right` | `A` | `UIO[Either[Nothing, A]]` | An `Either` can be converted into a ZIO effect using `ZIO.fromEither`: ```scala val zeither = ZIO.fromEither(Right("Success!")) ``` The error type of the resulting effect will be whatever type the `Left` case has, while the success type will be whatever type the `Right` case has. #### Try | Function | Input Type | Output Type | |-----------|---------------------|-------------| | `fromTry` | `scala.util.Try[A]` | `Task[A]` | A `Try` value can be converted into a ZIO effect using `ZIO.fromTry`: ```scala val ztry = ZIO.fromTry(Try(42 / 0)) ``` The error type of the resulting effect will always be `Throwable`, because `Try` can only fail with values of type `Throwable`. #### Future | Function | Input Type | Output Type | |-----------------------|--------------------------------------------------|--------------------| | `fromFuture` | `ExecutionContext => scala.concurrent.Future[A]` | `Task[A]` | | `fromFutureJava` | `java.util.concurrent.Future[A]` | `RIO[Blocking, A]` | | `fromFunctionFuture` | `R => scala.concurrent.Future[A]` | `RIO[R, A]` | | `fromFutureInterrupt` | `ExecutionContext => scala.concurrent.Future[A]` | `Task[A]` | A `Future` can be converted into a ZIO effect using `ZIO.fromFuture`: ```scala lazy val future = Future.successful("Hello!") val zfuture: Task[String] = ZIO.fromFuture { implicit ec => future.map(_ => "Goodbye!") } ``` The function passed to `fromFuture` is passed an `ExecutionContext`, which allows ZIO to manage where the `Future` runs (of course, we can ignore this `ExecutionContext`). The error type of the resulting effect will always be `Throwable`, because `Future` can only fail with values of type `Throwable`. #### Promise | Function | Input Type | Output Type | |--------------------|-------------------------------|-------------| | `fromPromiseScala` | `scala.concurrent.Promise[A]` | `Task[A]` | A `Promise` can be converted into a ZIO effect using `ZIO.fromPromiseScala`: ```scala val func: String => String = s => s.toUpperCase for { promise <- ZIO.succeed(scala.concurrent.Promise[String]()) _ <- ZIO.attempt { Try(func("hello world from future")) match { case Success(value) => promise.success(value) case Failure(exception) => promise.failure(exception) } }.fork value <- ZIO.fromPromiseScala(promise) _ <- Console.printLine(s"Hello World in UpperCase: $value") } yield () ``` #### Fiber | Function | Input Type | Output Type | |----------------|----------------------|-------------| | `fromFiber` | `Fiber[E, A]` | `IO[E, A]` | | `fromFiberZIO` | `IO[E, Fiber[E, A]]` | `IO[E, A]` | A `Fiber` can be converted into a ZIO effect using `ZIO.fromFiber`: ```scala val io: IO[Nothing, String] = ZIO.fromFiber(Fiber.succeed("Hello from Fiber!")) ``` ### From Side-Effects ZIO can convert both synchronous and asynchronous side-effects into ZIO effects (pure values). These functions can be used to wrap procedural code, allowing us to seamlessly use all features of ZIO with legacy Scala and Java code, as well as third-party libraries. #### Synchronous | Function | Input Type | Output Type | Note | |-----------|------------|-------------|---------------------------------------------| | `succeed` | `A` | `UIO[A]` | Imports a total synchronous effect | | `attempt` | `A` | Task[A] | Imports a (partial) synchronous side-effect | A synchronous side-effect can be converted into a ZIO effect using `ZIO.attempt`: ```scala val getLine: Task[String] = ZIO.attempt(StdIn.readLine()) ``` The error type of the resulting effect will always be `Throwable`, because side-effects may throw exceptions with any value of type `Throwable`. If a given side-effect is known to not throw any exceptions, then the side-effect can be converted into a ZIO effect using `ZIO.succeed`: ```scala def printLine(line: String): UIO[Unit] = ZIO.succeed(println(line)) val succeedTask: UIO[Long] = ZIO.succeed(java.lang.System.nanoTime()) ``` We should be careful when using `ZIO.succeed`—when in doubt about whether or not a side-effect is total, prefer `ZIO.attempt` to convert the effect. If this is too broad, the `refineOrDie` method of `ZIO` may be used to retain only certain types of exceptions, and to die on any other types of exceptions: ```scala val printLine2: IO[IOException, String] = ZIO.attempt(scala.io.StdIn.readLine()).refineToOrDie[IOException] ``` ##### Blocking Synchronous Side-Effects | Function | Input Type | Output Type | |-----------------------------|-------------------------------------|---------------------------------| | `blocking` | `ZIO[R, E, A]` | `ZIO[R, E, A]` | | `attemptBlocking` | `A` | `RIO[Blocking, A]` | | `attemptBlockingCancelable` | `effect: => A`, `cancel: UIO[Unit]` | `RIO[Blocking, A]` | | `attemptBlockingInterrupt` | `A` | `RIO[Blocking, A]` | | `attemptBlockingIO` | `A` | `ZIO[Blocking, IOException, A]` | By default, ZIO is asynchronous and all effects will be executed on a default primary thread pool which is optimized for asynchronous operations. As ZIO uses a fiber-based concurrency model, if we run **Blocking I/O** or **CPU Work** workloads on a primary thread pool, they are going to monopolize all threads of **primary thread pool**. ZIO has a separate **blocking thread pool** specially designed for **Blocking I/O** and, also **CPU Work** workloads. We should run blocking workloads on this thread pool by using `ZIO.blocking` or `ZIO.attemptBlocking*` constructors to prevent interfering with the primary thread pool. :::note ZIO has an auto-blocking mechanism that detects blocking operations and runs them on a separate blocking thread pool. However, if you know that some code is blocking you can use the `ZIO.blocking` constructor to give a "hint" of this to the ZIO runtime. ::: The `blocking` operator takes a ZIO effect and returns another effect that is going to run on a blocking thread pool: ```scala val program = ZIO.foreachPar((1 to 100).toArray)(t => ZIO.blocking(blockingTask(t))) ``` A blocking side-effect can be converted directly into a ZIO effect using the `attemptBlocking` method: ```scala def blockingTask(n: Int) = ZIO.attemptBlocking { do { println(s"Running blocking task number $n on dedicated blocking thread pool") Thread.sleep(3000) } while (true) } ``` The resulting effect will be executed on a separate thread pool designed specifically for blocking effects. Blocking side-effects can be interrupted by invoking `Thread.interrupt` using the `attemptBlockingInterrupt` method. Some blocking side-effects can only be interrupted by invoking a cancellation effect. We can convert these side-effects using the `attemptBlockingCancelable` method: ```scala def accept(l: ServerSocket) = ZIO.attemptBlockingCancelable(l.accept())(ZIO.succeed(l.close())) ``` If a side-effect has already been converted into a ZIO effect, then instead of `attemptBlocking`, the `blocking` method can be used to ensure the effect will be executed on the blocking thread pool: ```scala def download(url: String) = ZIO.attempt { Source.fromURL(url)(Codec.UTF8).mkString } def safeDownload(url: String) = ZIO.blocking(download(url)) ``` #### Asynchronous | Function | Input Type | Output Type | |------------------|----------------------------------------------------------------|----------------| | `async` | `(ZIO[R, E, A] => Unit) => Any` | `ZIO[R, E, A]` | | `asyncZIO` | `(ZIO[R, E, A] => Unit) => ZIO[R, E, Any]` | `ZIO[R, E, A]` | | `asyncMaybe` | `(ZIO[R, E, A] => Unit) => Option[ZIO[R, E, A]]` | `ZIO[R, E, A]` | | `asyncInterrupt` | `(ZIO[R, E, A] => Unit) => Either[URIO[R, Any], ZIO[R, E, A]]` | `ZIO[R, E, A]` | An asynchronous side-effect with a callback-based API can be converted into a ZIO effect using `ZIO.async`: ```scala object legacy { def login( onSuccess: User => Unit, onFailure: AuthError => Unit): Unit = ??? } val login: IO[AuthError, User] = ZIO.async[Any, AuthError, User] { callback => legacy.login( user => callback(ZIO.succeed(user)), err => callback(ZIO.fail(err)) ) } ``` Asynchronous ZIO effects are much easier to use than callback-based APIs, and they benefit from ZIO features like interruption, resource-safety, and superior error handling. ### Creating Suspended Effects | Function | Input Type | Output Type | |------------------|----------------|----------------| | `suspend` | `RIO[R, A]` | `RIO[R, A]` | | `suspendSucceed` | `ZIO[R, E, A]` | `ZIO[R, E, A]` | A `RIO[R, A]` effect can be suspended using `suspend` function: ```scala val suspendedEffect: RIO[Any, ZIO[Any, IOException, Unit]] = ZIO.suspend(ZIO.attempt(Console.printLine("Suspended Hello World!"))) ``` ## Mapping ### map We can change an `IO[E, A]` to an `IO[E, B]` by calling the `map` method with a function `A => B`. This lets us transform values produced by actions into other values. ```scala val mappedValue: UIO[Int] = ZIO.succeed(21).map(_ * 2) ``` ## Tapping Using `ZIO.tap` we can peek into a success value and perform any effectful operation, without changing the returning value of the original effect: ```scala trait ZIO[-R, +E, +A] { def tap[R1 <: R, E1 >: E](f: A => ZIO[R1, E1, Any]): ZIO[R1, E1, A] def tapSome[R1 <: R, E1 >: E](f: PartialFunction[A, ZIO[R1, E1, Any]]): ZIO[R1, E1, A] } ``` ```scala object MainApp extends ZIOAppDefault { def isPrime(n: Int): Boolean = if (n <= 1) false else (2 until n).forall(i => n % i != 0) val myApp: ZIO[Any, IOException, Unit] = for { ref <- Ref.make(List.empty[Int]) prime <- Random .nextIntBetween(0, Int.MaxValue) .tap(random => ref.update(_ :+ random)) .repeatUntil(isPrime) _ <- Console.printLine(s"found a prime number: $prime") tested <- ref.get _ <- Console.printLine( s"list of tested numbers: ${tested.mkString(", ")}" ) } yield () def run = myApp } ``` ## Chaining We can execute two actions in sequence with the `flatMap` method. The second action may depend on the value produced by the first action. ```scala val chainedActionsValue: UIO[List[Int]] = ZIO.succeed(List(1, 2, 3)).flatMap { list => ZIO.succeed(list.map(_ + 1)) } ``` If the first effect fails, the callback passed to `flatMap` will never be invoked, and the composed effect returned by `flatMap` will also fail. In _any_ chain of effects, the first failure will short-circuit the whole chain, just like throwing an exception will prematurely exit a sequence of statements. Because the `ZIO` data type supports both `flatMap` and `map`, we can use Scala's _for comprehensions_ to build sequential effects: ```scala val program = for { _ <- Console.printLine("Hello! What is your name?") name <- Console.readLine _ <- Console.printLine(s"Hello, ${name}, welcome to ZIO!") } yield () ``` _For comprehensions_ provide a more procedural syntax for composing chains of effects. ## Zipping We can combine two effects into a single effect with the `zip` method. The resulting effect succeeds with a tuple that contains the success values of both effects: ```scala val zipped: UIO[(String, Int)] = ZIO.succeed("4").zip(ZIO.succeed(2)) ``` Note that `zip` operates sequentially: the effect on the left side is executed before the effect on the right side. In any `zip` operation, if either the left or right-hand sides fail, then the composed effect will fail, because _both_ values are required to construct the tuple. ### zipLeft and zipRight Sometimes, when the success value of an effect is not useful (for example, it is `Unit`), it can be more convenient to use the `zipLeft` or `zipRight` functions, which first perform a `zip`, and then map over the tuple to discard one side or the other: ```scala val zipRight1 = Console.printLine("What is your name?").zipRight(Console.readLine) ``` The `zipRight` and `zipLeft` functions have symbolic aliases, known as `*>` and `<*`, respectively. Some developers find these operators easier to read: ```scala val zipRight2 = Console.printLine("What is your name?") *> Console.readLine ``` ## Parallelism ZIO provides many operations for performing effects in parallel. These methods are all named with a `Par` suffix that helps us identify opportunities to parallelize our code. For example, the ordinary `ZIO#zip` method zips two effects together, sequentially. But there is also a `ZIO#zipPar` method, which zips two effects together in parallel. The following table summarizes some of the sequential operations and their corresponding parallel versions: | **Description** | **Sequential** | **Parallel** | | ---------------------------: | :--------------: | :-----------------: | | Zip two effects into one | `ZIO#zip` | `ZIO#zipPar` | | Zip two effects into one | `ZIO#zipWith` | `ZIO#zipWithPar` | | Collect from many effects | `ZIO.collectAll` | `ZIO.collectAllPar` | | Effectfully loop over values | `ZIO.foreach` | `ZIO.foreachPar` | | Reduce many values | `ZIO.reduceAll` | `ZIO.reduceAllPar` | | Merge many values | `ZIO.mergeAll` | `ZIO.mergeAllPar` | For all the parallel operations, if one effect fails, then others will be interrupted, to minimize unnecessary computation. If the fail-fast behavior is not desired, potentially failing effects can be first converted into infallible effects using the `ZIO#either` or `ZIO#option` methods. ### Racing ZIO lets us race multiple effects in parallel, returning the first successful result: ```scala for { winner <- ZIO.succeed("Hello").race(ZIO.succeed("Goodbye")) } yield winner ``` If we want the first success or failure, rather than the first success, then we can use `left.either race right.either`, for any effects `left` and `right`. ## Timeout ZIO lets us timeout any effect using the `ZIO#timeout` method, which returns a new effect that succeeds with an `Option`. A value of `None` indicates the timeout elapsed before the effect completed. ```scala ZIO.succeed("Hello").timeout(10.seconds) ``` If an effect times out, then instead of continuing to execute in the background, it will be interrupted so no resources will be wasted. ## Error Management ### Either | Function | Input Type | Output Type | | ------------- | ------------------------- | ----------------------- | | `ZIO#either` | `ZIO[R, E, A]` | `URIO[R, Either[E, A]]` | | `ZIO.absolve` | `ZIO[R, E, Either[E, A]]` | `ZIO[R, E, A]` | We can surface failures with `ZIO#either`, which takes a `ZIO[R, E, A]` and produces a `ZIO[R, Nothing, Either[E, A]]`. ```scala val zeither: UIO[Either[String, Int]] = ZIO.fail("Uh oh!").either ``` We can submerge failures with `ZIO.absolve`, which is the opposite of `either` and turns a `ZIO[R, Nothing, Either[E, A]]` into a `ZIO[R, E, A]`: ```scala def sqrt(io: UIO[Double]): IO[String, Double] = ZIO.absolve( io.map(value => if (value < 0.0) Left("Value must be >= 0.0") else Right(Math.sqrt(value)) ) ) ``` ### Catching | Function | Input Type | Output Type | | --------------------- | ----------------------------------------------------------- | ----------------- | | `ZIO#catchAll` | `E => ZIO[R1, E2, A1]` | `ZIO[R1, E2, A1]` | | `ZIO#catchAllCause` | `Cause[E] => ZIO[R1, E2, A1]` | `ZIO[R1, E2, A1]` | | `ZIO#catchAllDefect` | `Throwable => ZIO[R1, E1, A1]` | `ZIO[R1, E1, A1]` | | `ZIO#catchAllTrace` | `((E, Option[StackTrace])) => ZIO[R1, E2, A1]` | `ZIO[R1, E2, A1]` | | `ZIO#catchSome` | `PartialFunction[E, ZIO[R1, E1, A1]]` | `ZIO[R1, E1, A1]` | | `ZIO#catchSomeCause` | `PartialFunction[Cause[E], ZIO[R1, E1, A1]]` | `ZIO[R1, E1, A1]` | | `ZIO#catchSomeDefect` | `PartialFunction[Throwable, ZIO[R1, E1, A1]]` | `ZIO[R1, E1, A1]` | | `ZIO#catchSomeTrace` | `PartialFunction[(E, Option[StackTrace]), ZIO[R1, E1, A1]]` | `ZIO[R1, E1, A1]` | #### Catching All Errors If we want to catch and recover from all types of errors and effectfully attempt recovery, we can use the `catchAll` method: ```scala val z: IO[IOException, Array[Byte]] = readFile("primary.json").catchAll(_ => readFile("backup.json")) ``` In the callback passed to `catchAll`, we may return an effect with a different error type (or perhaps `Nothing`), which will be reflected in the type of effect returned by `catchAll`. #### Catching Some Errors If we want to catch and recover from only some types of exceptions and effectfully attempt recovery, we can use the `catchSome` method: ```scala val data: IO[IOException, Array[Byte]] = readFile("primary.data").catchSome { case _ : FileNotFoundException => readFile("backup.data") } ``` Unlike `catchAll`, `catchSome` cannot reduce or eliminate the error type, although it can widen the error type to a broader class of errors. ### Fallback | Function | Input Type | Output Type | | ---------------- | ------------------------- | --------------------------- | | `orElse` | `ZIO[R1, E2, A1]` | `ZIO[R1, E2, A1]` | | `orElseEither` | `ZIO[R1, E2, B]` | `ZIO[R1, E2, Either[A, B]]` | | `orElseFail` | `E1` | `ZIO[R, E1, A]` | | `orElseOptional` | `ZIO[R1, Option[E1], A1]` | `ZIO[R1, Option[E1], A1]` | | `orElseSucceed` | `A1` | `URIO[R, A1]` | We can try one effect, or, if it fails, try another effect, with the `orElse` combinator: ```scala val primaryOrBackupData: IO[IOException, Array[Byte]] = readFile("primary.data").orElse(readFile("backup.data")) ``` ### Folding | Function | Input Type | Output Type | | -------------- | ------------------------------------------------------------------------------------ | ---------------- | | `fold` | `failure: E => B, success: A => B` | `URIO[R, B]` | | `foldCause` | `failure: Cause[E] => B, success: A => B` | `URIO[R, B]` | | `foldZIO` | `failure: E => ZIO[R1, E2, B], success: A => ZIO[R1, E2, B]` | `ZIO[R1, E2, B]` | | `foldCauseZIO` | `failure: Cause[E] => ZIO[R1, E2, B], success: A => ZIO[R1, E2, B]` | `ZIO[R1, E2, B]` | | `foldTraceZIO` | `failure: ((E, Option[StackTrace])) => ZIO[R1, E2, B], success: A => ZIO[R1, E2, B]` | `ZIO[R1, E2, B]` | Scala's `Option` and `Either` data types have `fold`, which lets us handle both failure and success at the same time. In a similar fashion, `ZIO` effects also have several methods that allow us to handle both failure and success. The first fold method, `fold`, lets us non-effectfully handle both failure and success by supplying a non-effectful handler for each case: ```scala lazy val DefaultData: Array[Byte] = Array(0, 0) val primaryOrDefaultData: UIO[Array[Byte]] = readFile("primary.data").fold( _ => DefaultData, data => data) ``` The second fold method, `foldZIO`, lets us effectfully handle both failure and success by supplying an effectful (but still pure) handler for each case: ```scala val primaryOrSecondaryData: IO[IOException, Array[Byte]] = readFile("primary.data").foldZIO( _ => readFile("secondary.data"), data => ZIO.succeed(data)) ``` Nearly all error handling methods are defined in terms of `foldZIO`, because it is both powerful and fast. In the following example, `foldZIO` is used to handle both failure and success of the `readUrls` method: ```scala val urls: UIO[Content] = readUrls("urls.json").foldZIO( error => ZIO.succeed(NoContent(error)), success => fetchContent(success) ) ``` ### Retrying | Function | Input Type | Output Type | | ------------------- | -------------------------------------------------------------------- | --------------------------- | | `retry` | `Schedule[R1, E, S]` | `ZIO[R1, E, A]` | | `retryN` | `n: Int` | `ZIO[R, E, A]` | | `retryOrElse` | `policy: Schedule[R1, E, S], orElse: (E, S) => ZIO[R1, E1, A1]` | `ZIO[R1, E1, A1]` | | `retryOrElseEither` | `schedule: Schedule[R1, E, Out], orElse: (E, Out) => ZIO[R1, E1, B]` | `ZIO[R1, E1, Either[B, A]]` | | `retryUntil` | `E => Boolean` | `ZIO[R, E, A]` | | `retryUntilEquals` | `E1` | `ZIO[R, E1, A]` | | `retryUntilZIO` | `E => URIO[R1, Boolean]` | `ZIO[R1, E, A]` | | `retryWhile` | `E => Boolean` | `ZIO[R, E, A]` | | `retryWhileEquals` | `E1` | `ZIO[R, E1, A]` | | `retryWhileZIO` | `E => URIO[R1, Boolean]` | `ZIO[R1, E, A]` | When we are building applications we want to be resilient in the face of a transient failure. This is where we need to retry to overcome these failures. There are a number of useful methods on the ZIO data type for retrying failed effects. The most basic of these is `ZIO#retry`, which takes a `Schedule` and returns a new effect that will retry the first effect if it fails according to the specified policy: ```scala val retriedOpenFile: ZIO[Any, IOException, Array[Byte]] = readFile("primary.data").retry(Schedule.recurs(5)) ``` The next most powerful function is `ZIO#retryOrElse`, which allows specification of a fallback to use if the effect does not succeed with the specified policy: ```scala readFile("primary.data").retryOrElse( Schedule.recurs(5), (_, _:Long) => ZIO.succeed(DefaultData) ) ``` The final method, `ZIO#retryOrElseEither`, allows returning a different type for the fallback. ## Resource Management ZIO's resource management features work across synchronous, asynchronous, concurrent, and other effect types, and provide strong guarantees even in the presence of failure, interruption, or defects in the application. ### Finalizing Scala has a `try` / `finally` construct which helps us to make sure we don't leak resources because no matter what happens in the `try`, the `finally` block will be executed. So we can open files in the `try` block, and then we can close them in the `finally` block, and that gives us the guarantee that we will not leak resources. #### Asynchronous Try / Finally The problem with the `try` / `finally` construct is that it only applies to synchronous code, meaning it doesn't work for asynchronous code. ZIO gives us a method called `ensuring` that works with either synchronous or asynchronous actions. So we have a functional `try` / `finally` even for asynchronous regions of our code. Like `try` / `finally`, the `ensuring` operation guarantees that if an effect begins executing and then terminates (for whatever reason), then the finalizer will begin executing: ```scala val finalizer = ZIO.succeed(println("Finalizing!")) val finalized: IO[String, Unit] = ZIO.fail("Failed!").ensuring(finalizer) ``` The finalizer is not allowed to fail, which means that it must handle any errors internally. Like `try` / `finally`, finalizers can be nested, and the failure of any inner finalizer will not affect outer finalizers. Nested finalizers will be executed in reverse order, and linearly (not in parallel). Unlike `try` / `finally`, `ensuring` works across all types of effects, including asynchronous and concurrent effects. Here is another example of ensuring that our clean-up action is called before our effect is done: ```scala var i: Int = 0 val action: Task[String] = ZIO.succeed(i += 1) *> ZIO.fail(new Throwable("Boom!")) val cleanupAction: UIO[Unit] = ZIO.succeed(i -= 1) val composite = action.ensuring(cleanupAction) ``` :::caution Finalizers offer very powerful guarantees, but they are low-level, and should generally not be used for releasing resources. For higher-level logic built on `ensuring`, see `ZIO#acquireReleaseWith` in the acquire release section. ::: #### Unstoppable Finalizers In Scala when we nest `try` / `finally` finalizers, they cannot be stopped. If we have nested finalizers and one of them fails for some sort of catastrophic reason the ones on the outside will still be run and in the correct order. ```scala try { try { try { ... } finally f1 } finally f2 } finally f3 ``` Also in ZIO like `try` / `finally`, the finalizers are unstoppable. This means if we have a buggy finalizer that is going to leak some resources, we will leak the minimum amount of resources because all other finalizers will still be run in the correct order. ```scala val io = ??? io.ensuring(f1) .ensuring(f2) .ensuring(f3) ``` ### Acquire Release In Scala `try` / `finally` is often used to manage resources. A common use for `try` / `finally` is safely acquiring and releasing resources, such as new socket connections or opened files: ```scala val handle = openFile(name) try { processFile(handle) } finally closeFile(handle) ``` ZIO encapsulates this common pattern with `ZIO#acquireRelease`, which allows us to specify an _acquire_ effect, which acquires a resource; a _release_ effect, which releases it; and a _use_ effect, which uses the resource. Acquire release lets us open a file and close the file and no matter what happens when we are using that resource. The release action is guaranteed to be executed by the runtime system, even if the utilize action throws an exception or the executing fiber is interrupted. Acquire release is a built-in primitive that let us safely acquire and release resources. It is used for a similar purpose as `try` / `catch` / `finally`, only acquire release work with synchronous and asynchronous actions, work seamlessly with fiber interruption, and is built on a different error model that ensures no errors are ever swallowed. Acquire release consist of an _acquire_ action, a _utilize_ action (which uses the acquired resource), and a _release_ action. ```scala val groupedFileData: IO[IOException, Unit] = ZIO.acquireReleaseWith(openFile("data.json"))(closeFile(_)) { file => for { data <- decodeData(file) grouped <- groupData(data) } yield grouped } ``` Acquire releases have compositional semantics, so if an acquire release is nested inside another acquire release, and the outer resource is acquired, then the outer release will always be called, even if, for example, the inner release fails. Let's look at a full working example on using acquire release: ```scala object Main extends ZIOAppDefault { // run my acquire release def run = myAcquireRelease def closeStream(is: FileInputStream) = ZIO.succeed(is.close()) def convertBytes(is: FileInputStream, len: Long) = ZIO.attempt { val buffer = new Array[Byte](len.toInt) is.read(buffer) println(new String(buffer, StandardCharsets.UTF_8)) } // myAcquireRelease is just a value. Won't execute anything here until interpreted val myAcquireRelease: Task[Unit] = for { file <- ZIO.attempt(new File("/tmp/hello")) len = file.length string <- ZIO.acquireReleaseWith(ZIO.attempt(new FileInputStream(file)))(closeStream)(convertBytes(_, len)) } yield string } ``` ## Caching and Memoization Memoization caches the result of an effect or function computation, preventing redundant calculations when the same input is requested multiple times. This section covers indefinite memoization with `ZIO#memoize` and `ZIO.memoize`. For time-limited caching, see the "Time-Limited Caching" subsection below. ### Memoizing Effects To memoize an effect and cache its result—useful when the same expensive computation may be executed multiple times—call `memoize` on it: ```scala val expensiveComputation: ZIO[Any, Nothing, Int] = ZIO.succeed(42) val memoized: ZIO[Any, Nothing, ZIO[Any, Nothing, Int]] = expensiveComputation.memoize ``` The memoized effect produces a cached effect that runs the original computation only once. Subsequent calls return the cached result: ```scala def computeValue: ZIO[Any, Nothing, Int] = { ZIO.succeed { println("Computing...") 42 } } object Example extends ZIOAppDefault { def run = for { memoized <- computeValue.memoize _ <- memoized // prints "Computing..." _ <- memoized // returns cached result, no print _ <- memoized // returns cached result, no print } yield () } ``` :::info When a fiber computing a memoized value is interrupted, the result is discarded and awaiting fibers transparently retry the computation. This ensures that interruption of one fiber does not propagate to others waiting for the same memoized result. ::: ### Memoizing Functions To create a memoized version of a function that returns a `ZIO` effect, use the `ZIO.memoize` constructor, which caches results based on input arguments: ```scala val expensiveLookup: String => ZIO[Any, Nothing, Int] = key => ZIO.succeed(key.length) for { memoized <- ZIO.memoize(expensiveLookup) result1 <- memoized("hello") // computes and caches result2 <- memoized("hello") // returns cached result result3 <- memoized("world") // different input, computes anew } yield (result1, result2, result3) ``` ### Time-Limited Caching Use `ZIO#cached` to cache the result of an effect with an automatic expiration time. This is useful when results have a limited lifetime and should be refreshed periodically. The cache is thread-safe and supports concurrent access from multiple fibers. Note: `IO[E, A]` used in this section is a type alias for `ZIO[Any, E, A]`, representing an effect that has no environment requirements. #### Basic Caching with `cached` Call `cached` with a time-to-live duration to create a cached version of an effect. The `cached` method returns an effect that produces an `IO` (which is a type alias for `ZIO[Any, E, A]`). When you execute the returned `IO`, it will run the original effect once and cache the result for the specified duration: ```scala val expensiveData: ZIO[Any, Nothing, String] = ZIO.succeed("data") for { // cached is of type IO[Nothing, String] (equivalent to ZIO[Any, Nothing, String]) cachedIO <- expensiveData.cached(5.minutes) result1 <- cachedIO // runs computation and caches result result2 <- cachedIO // returns cached result (within 5 minutes) } yield (result1, result2) ``` The return type is `ZIO[Any, Nothing, IO[Nothing, String]]`, which means `cached` returns an effect that, when executed, produces a cached `IO` effect that you can reuse multiple times. When the time-to-live duration expires, the cache is invalidated and the effect runs again on the next call: ```scala def fetchUserData: ZIO[Any, Nothing, String] = ZIO.succeed("user-data") for { cached <- fetchUserData.cached(5.minutes) _ <- cached // runs and caches _ <- ZIO.sleep(6.minutes) result <- cached // TTL expired, recomputes } yield result ``` **Comparison with `memoize`**: Unlike `ZIO.memoize` which caches results indefinitely (based on function arguments), `cached` provides time-limited caching with automatic expiration. Use `cached` when you need periodic refresh of results, and `memoize` when you want permanent caching of expensive computations. #### Caching with Manual Invalidation Call `cachedInvalidate` to obtain both the cached effect and a separate effect for manually invalidating the cache before its TTL expires (useful when you need to cache-bust based on external events), returning a tuple of the cached effect and an invalidation function: ```scala def freshData: ZIO[Any, Nothing, String] = ZIO.succeed("data") for { pair <- freshData.cachedInvalidate(1.hour) (cached, invalidate) = pair result1 <- cached // runs and caches result2 <- cached // returns cached result _ <- invalidate // manually clear cache before TTL expires result3 <- cached // recomputes since cache was invalidated } yield (result1, result2, result3) ``` #### Concurrent Access Multiple fibers can safely await the same cached result. The first fiber triggers computation while others wait for the result. This ensures the underlying effect runs only once even with concurrent access: ```scala def expensiveComputation: ZIO[Any, Nothing, Int] = ZIO.succeed { println("Computing...") 42 } for { cached <- expensiveComputation.cached(5.minutes) fiber1 <- cached.fork fiber2 <- cached.fork fiber3 <- cached.fork result1 <- fiber1.join // one executes the computation result2 <- fiber2.join // others wait for the same result result3 <- fiber3.join // all get 42, but computed only once } yield (result1, result2, result3) ``` :::note The cache uses a `Ref.Synchronized` internally to manage state safely, ensuring that only one computation runs at a time even when multiple fibers call the cached effect concurrently. This guarantees thread-safe, consistent behavior. ::: ## ZIO Aspect There are two types of concerns in an application, _core concerns_, and _cross-cutting concerns_. Cross-cutting concerns are shared among different parts of our application. We usually find them scattered and duplicated across our application, or they are tangled up with our primary concerns. This reduces the level of modularity of our programs. A cross-cutting concern is more about _how_ we do something than _what_ we are doing. For example, when we are downloading a bunch of files, creating a socket to download each one is the core concern because it is a question of _what_ rather than the _how_, but the following concerns are cross-cutting ones: - Downloading files _sequentially_ or in _parallel_ - _Retrying_ and _timing out_ the download process - _Logging_ and _monitoring_ the download process So they don't affect the return type of our workflows, but they add some new aspects or change their behavior. To increase the modularity of our applications, we can separate cross-cutting concerns from the main logic of our programs. ZIO supports this programming paradigm, which is called _aspect-oriented programming_. The `ZIO` effect has a data type called `ZIOAspect`, which allows modifying a `ZIO` effect and converting it into a specialized `ZIO` effect. We can add a new aspect to a `ZIO` effect with `@@` syntax like this: ```scala val myApp: ZIO[Any, Throwable, String] = ZIO.attempt("Hello!") @@ ZIOAspect.debug ``` As we see, the `debug` aspect doesn't change the return type of our effect, but it adds a new debugging aspect to our effect. `ZIOAspect` is like a transformer of the `ZIO` effect, which takes a `ZIO` effect and converts it to another `ZIO` effect. We can think of a `ZIOAspect` as a function of type `ZIO[R, E, A] => ZIO[R, E, A]`. To compose multiple aspects, we can use `@@` operator: ```scala def download(url: String): ZIO[Any, Throwable, Chunk[Byte]] = ZIO.succeed(???) ZIO.foreachPar(List("zio.dev", "google.com")) { url => download(url) @@ ZIOAspect.retry(Schedule.fibonacci(1.seconds)) @@ ZIOAspect.loggedWith[Chunk[Byte]](file => s"Downloaded $url file with size of ${file.length} bytes") } ``` The order of aspect composition matters. Therefore, if we change the order, the behavior may change. --- ## ZIOApp The `ZIOApp` trait is an entry point for a ZIO application that allows sharing layers between applications. It also provides us the ability to compose multiple ZIO applications. There is another simpler version of `ZIOApp` called `ZIOAppDefault`. We usually use `ZIOAppDefault` which uses the default ZIO environment (`ZEnv`). ## Running a ZIO effect The `ZIOAppDefault` has a `run` function, which is the main entry point for running a ZIO application on the JVM: ```scala object MyApp extends ZIOAppDefault { def run = for { _ <- Console.printLine("Hello! What is your name?") n <- Console.readLine _ <- Console.printLine("Hello, " + n + ", good to meet you!") } yield () } ``` ## Accessing Command-line Arguments ZIO has a service that contains command-line arguments of an application called `ZIOAppArgs`. We can access command-line arguments using the built-in `getArgs` method: ```scala object HelloApp extends ZIOAppDefault { def run = for { args <- getArgs _ <- if (args.isEmpty) Console.printLine("Please provide your name as an argument") else Console.printLine(s"Hello, ${args.head}!") } yield () } ``` ## Customized Runtime In the ZIO app, by overriding its `bootstrap` value, we can map the current runtime to a customized one. Let's customize it by introducing our own executor: ```scala object CustomizedRuntimeZIOApp extends ZIOAppDefault { override val bootstrap = Runtime.setExecutor( Executor.fromThreadPoolExecutor( new ThreadPoolExecutor( 5, 10, 5000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue[Runnable]() ) ) ) def run = myAppLogic } ``` A detailed explanation of the ZIO runtime system can be found on the [runtime](runtime.md) page. ## Installing Low-level Functionalities We can hook into the ZIO runtime to install low-level functionalities into the ZIO application, such as _logging_, _profiling_, and other similar foundational pieces of infrastructure. A detailed explanation can be found on the [runtime](runtime.md) page. ## Composing ZIO Applications To compose ZIO applications, we can use `<>` operator: ```scala object MyApp1 extends ZIOAppDefault { def run = ZIO.succeed(???) } object MyApp2 extends ZIOAppDefault { override val bootstrap: ZLayer[Any, Any, Any] = asyncProfiler ++ slf4j ++ loggly ++ newRelic def run = ZIO.succeed(???) } object Main extends ZIOApp.Proxy(MyApp1 <> MyApp2) ``` The `<>` operator combines the layers of the two applications and then runs the two applications in parallel. ## Graceful Shutdown Timeout When a ZIO application (e.g. one extending `ZIOAppDefault`) receives an external interruption signal such as **SIGINT** when pressing **Ctrl+C**, the runtime will attempt to run all finalizers (cleanup logic) before exiting. By default, `gracefulShutdownTimeout` set to `Duration.Infinity`, which means ZIO will wait indefinitely for finalizers unless you override it. Below are two examples: one where cleanup finishes within the timeout and one where cleanup deliberately exceeds it. ### Example 1: Finalizer completes within the timeout ```scala object MyApp extends ZIOAppDefault { // Wait at most 30 seconds for all finalizers to complete on SIGINT override def gracefulShutdownTimeout: Duration = 30.seconds val run: ZIO[ZIOAppArgs with Scope, Any, Any] = ZIO.acquireReleaseWith( acquire = ZIO.logInfo("Acquiring resource...").as("MyResource") )(release = _ => ZIO.logInfo("Releasing resource (3s) ...") *> ZIO.sleep(3.seconds) *> ZIO.logInfo("Cleanup done") ) { resource => ZIO.logInfo(s"Running with $resource, press Ctrl+C to interrupt") *> ZIO.never } } ``` In this example, `MyApp` starts and logs `Acquiring resource...`. When you press Ctrl+C (sending SIGINT), ZIO interrupts the main fiber and immediately runs the finalizer which logs `Releasing resource (3s) ...` and then sleeps for three seconds. Because the finalizer completes its work well within the 30s timeout, the runtime finishes cleanup and the process exits normally. ### Example 2: Finalizer exceeds the timeout ```scala object MyAppTimeout extends ZIOAppDefault { // Wait at most 5 seconds for finalizers to complete on SIGINT override def gracefulShutdownTimeout: Duration = 5.seconds val run: ZIO[ZIOAppArgs with Scope, Any, Any] = ZIO.acquireReleaseWith( acquire = ZIO.logInfo("Acquiring resource...").as("MyResource") )(release = _ => ZIO.logInfo("Releasing resource (20s) ...") *> ZIO.sleep(20.seconds) *> ZIO.logInfo("Cleanup done") ) { resource => ZIO.logInfo(s"Running with $resource, press Ctrl+C to interrupt") *> ZIO.never } } ``` Here, `MyAppTimeout` starts and logs `Acquiring resource...`. If you press Ctrl+C after a few seconds, ZIO interrupts the main fiber and starts running the finalizer, logging `Releasing resource (20s) ...` before sleeping for twenty seconds. However, `gracefulShutdownTimeout` is set to just five seconds, ZIO waits those five seconds and then prints exactly: ```bash **** WARNING **** Timed out waiting for ZIO application to shut down after 5 seconds. You can adjust your application's shutdown timeout by overriding the `shutdownTimeout` method ``` At that point, the JVM process exits immediately even though the 20-second finalizer has not yet finished. :::note Currently, `gracefulShutdownTimeout` is implemented for the **JVM** and **Scala Native** only ::: --- ## Automatic Layer Construction ZIO also has an automatic layer construction facility, which takes care of building dependency graphs from the individual layers and building blocks. So instead of manually composing layers together to build the final layer, we can only provide individual layers to the ZIO application, and it will do the rest. The automatic layer construction takes place at the _compile-time_, so if there is a problem in providing a layer, we will receive an error or warning message. So it helps us to diagnose the problem. Additionally, it has a way to print the dependency graph using built-in debug layers. ## Providing Individual Layers to a ZIO Application When we provide individual layers using `ZIO#provide`, `ZIO#provideCustom`, or `ZIO#provideSome` to a ZIO application, the compiler will create the dependency graph automatically from the provided layers: :::info We have a [separate section](dependency-propagation.md) that describes different methods for providing layers to the ZIO application. ::: Assume we have written the following services (`Cake`, `Chocolate`, `Flour`, and `Spoon`): ```scala trait Cake object Cake { val live: ZLayer[Chocolate & Flour, Nothing, Cake] = for { _ <- ZLayer.environment[Chocolate & Flour] cake <- ZLayer.succeed(new Cake {}) } yield cake } trait Spoon object Spoon { val live: ULayer[Spoon] = ZLayer.succeed(new Spoon {}) } trait Chocolate object Chocolate { val live: ZLayer[Spoon, Nothing, Chocolate] = ZLayer.service[Spoon].project(_ => new Chocolate {}) } trait Flour object Flour { val live: ZLayer[Spoon, Nothing, Flour] = ZLayer.service[Spoon].project(_ => new Flour {}) } ``` The `Cake` service has the following dependency graph: ``` Cake / \ Chocolate Flour | | Spoon Spoon ``` Now we can write an application that uses the `Cake` service as below: ```scala val myApp: ZIO[Cake, IOException, Unit] = for { cake <- ZIO.service[Cake] _ <- Console.printLine(s"Yay! I baked a cake with flour and chocolate: $cake") } yield () ``` The type of `myApp` indicates we should provide `Cake` to this ZIO application to run it. Let's give it that and see what happens: ```scala object MainApp extends ZIOAppDefault { def run = myApp.provide(Cake.live) } // error: // // ──── ZLAYER ERROR ──────────────────────────────────────────────────── // // Please provide layers for the following 2 types: // // Required by Cake.live // 1. Chocolate // 2. Flour // // ────────────────────────────────────────────────────────────────────── ``` Here are the errors that will be printed: ``` ──── ZLAYER ERROR ──────────────────────────────────────────────────── Please provide layers for the following 2 types: Required by Cake.live 1. Chocolate 2. Flour ────────────────────────────────────────────────────────────────────── ``` It says that we missed providing `Chocolate` and `Flour` layers. Now let's add these two missing layers: ```scala object MainApp extends ZIOAppDefault { def run = myApp.provide( Cake.live, Chocolate.live, Flour.live ) } // error: // // ──── ZLAYER ERROR ──────────────────────────────────────────────────── // // Please provide a layer for the following type: // // Required by Flour.live // 1. Spoon // // Required by Chocolate.live // 1. Spoon // // ────────────────────────────────────────────────────────────────────── ``` Again, the compiler asks us to provide another dependency called `Spoon`: ``` ──── ZLAYER ERROR ──────────────────────────────────────────────────── Please provide a layer for the following type: Required by Flour.live 1. Spoon Required by Chocolate.live 1. Spoon ────────────────────────────────────────────────────────────────────── ``` Finally, our application compiles without any errors: ```scala object MainApp extends ZIOAppDefault { def run = myApp.provide( Cake.live, Chocolate.live, Flour.live, Spoon.live ) } ``` Note that the order of dependencies doesn't matter. We can provide them in any order. Now, let's compare the automatic layer construction with the manual one: ```scala object MainApp extends ZIOAppDefault { val layers: ULayer[Cake] = (((Spoon.live >>> Chocolate.live) ++ (Spoon.live >>> Flour.live)) >>> Cake.live) def run = myApp.provideLayer(layers) } ``` ## Automatically Assembling Layers 1. **ZLayer.make[R]** — Using `ZLayer.make[R]`, we can provide a type `R` and then provide individual layers as arguments, it will automatically assemble these layers to create a layer of type `R`. For example, we can create a `Cake` layer as below: ```scala val cakeLayer: ZLayer[Any, Nothing, Cake] = ZLayer.make[Cake]( Cake.live, Chocolate.live, Flour.live, Spoon.live ) ``` We can also create a layer for intersections of services: ```scala val chocolateAndFlourLayer: ZLayer[Any, Nothing, Chocolate & Flour] = ZLayer.make[Chocolate & Flour]( Chocolate.live, Flour.live, Spoon.live ) ``` 2. **ZLayer.makeSome[R0, R]** — Automatically constructs a layer for the provided type `R`, leaving a remainder `R0`: ```scala val cakeLayer: ZLayer[Spoon, Nothing, Cake] = ZLayer.makeSome[Spoon, Cake]( Cake.live, Chocolate.live, Flour.live ) ``` ## ZLayer Debugging To debug ZLayer construction, we have two built-in layers, i.e., `ZLayer.Debug.tree` and `ZLayer.Debug.mermaid`. Let's include the `ZLayer.Debug.tree` layer into the layer construction: ```scala object MainApp extends ZIOAppDefault { def run = myApp.provide( Cake.live, Chocolate.live, Flour.live, Spoon.live, ZLayer.Debug.tree ) } ``` The following debug messages will be generated by the compiler: ``` [info] ZLayer Wiring Graph [info] [info] ◉ Cake.live [info] ├─◑ Chocolate.live [info] │ ╰─◑ Spoon.live [info] ╰─◑ Flour.live [info] ╰─◑ Spoon.live [info] ``` If we use the `ZLayer.Debug.mermaid` layer, it will generate the following debug messages: ``` [info] ZLayer Wiring Graph [info] [info] ◉ Cake.live [info] ├─◑ Chocolate.live [info] │ ╰─◑ Spoon.live [info] ╰─◑ Flour.live [info] ╰─◑ Spoon.live [info] [info] Mermaid Live Editor Link [info] https://mermaid-js.github.io/mermaid-live-editor/edit/#eyJjb2RlIjoiZ3JhcGhcbiAgICBDb25zb2xlLmxpdmVcbiAgICBDYWtlLmxpdmUgLS0+IENob2NvbGF0ZS5saXZlXG4gICAgQ2FrZS5saXZlIC0tPiBGbG91ci5saXZlXG4gICAgRmxvdXIubGl2ZSAtLT4gU3Bvb24ubGl2ZVxuICAgIFNwb29uLmxpdmVcbiAgICBDaG9jb2xhdGUubGl2ZSAtLT4gU3Bvb24ubGl2ZVxuICAgICIsIm1lcm1haWQiOiAie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwgInVwZGF0ZUVkaXRvciI6IHRydWUsICJhdXRvU3luYyI6IHRydWUsICJ1cGRhdGVEaWFncmFtIjogdHJ1ZX0= ``` --- ## Building Dependency Graph We have two options to build a dependency graph: 1. [Manual layer construction](manual-layer-construction.md)— This method uses ZIO's composition operators such as horizontal (`++`) and vertical (`>>>`) compositions. 2. [Automatic layer construction](automatic-layer-construction.md)— It uses metaprogramming to automatically create the dependency graph at compile time. Assume we have the following dependency graph with two top-level dependencies: ``` DocRepo ++ UserRepo ____/ | \____ / \ / | \ / \ Logging Database BlobStorage Logging Database | Logging ``` Now, assume that we have written an application that finally needs two services: `DocRepo` and `UserRepo`: ```scala val myApp: ZIO[DocRepo with UserRepo, Throwable, Unit] = ZIO.attempt(???) ``` 1. To create the dependency graph for this ZIO application manually, we can use the following code: ```scala val appLayer: URLayer[Any, DocRepo with UserRepo] = ((Logging.live ++ Database.live ++ (Logging.live >>> BlobStorage.live)) >>> DocRepo.live) ++ ((Logging.live ++ Database.live) >>> UserRepo.live) val res: ZIO[Any, Throwable, Unit] = myApp.provideLayer(appLayer) ``` 2. As the development of our application progress, the number of layers will grow, and maintaining the dependency graph manually could be tedious and hard to debug. So, we can automatically construct dependencies with friendly compile-time hints, using `ZIO#provide` operator: ```scala val res: ZIO[Any, Throwable, Unit] = myApp.provide( Logging.live, Database.live, BlobStorage.live, DocRepo.live, UserRepo.live ) ``` The order of dependencies doesn't matter: ```scala val res: ZIO[Any, Throwable, Unit] = myApp.provide( DocRepo.live, BlobStorage.live, Logging.live, Database.live, UserRepo.live ) ``` If we miss some dependencies, it doesn't compile, and the compiler gives us the clue: ```scala val app: ZIO[Any, Throwable, Unit] = myApp.provide( DocRepo.live, BlobStorage.live, // Logging.live, Database.live, UserRepo.live ) ``` ``` ZLayer Wiring Error ❯ missing Logging ❯ for DocRepo.live ❯ missing Logging ❯ for UserRepo.live ``` :::note The `ZIO#provide` method, together with its variant `ZIO#provideSome`, is default and easier way of injecting dependencies to the environmental effect. We do not require creating the dependency graph manually, it will be automatically generated. In contrast, the `ZIO#provideLayer`, and its variant `ZIO#provideSomeLayer`, is useful for low-level and custom cases. ::: --- ## Getting Started With Dependency Injection in ZIO :::caution In this page, we will focus on essential parts of dependency injection in ZIO. So in some examples we are not going to cover all the best practices for writing ZIO services. In real world applications, we encourage to use [service pattern](../service-pattern/service-pattern.md) to write ZIO services. ::: ## Essential Steps of Dependency Injection in ZIO We can achieve dependency injection through these three simple steps: 1. Accessing services from the ZIO environment through the `ZIO.serviceXYZ` operations. 2. Writing application logic using services and composing them together. 3. Building the dependency graph using manual or automatic layer construction (optional). 4. Providing dependencies to the ZIO environment through the `ZIO.provideXYZ` operations. ### Step 1: Accessing Services From The ZIO Environment To write application logic, we need to access services from the ZIO environment. We can do this by using the `ZIO.serviceXYZ` operation. For example, assume we have the following services: ```scala final class A { def foo: UIO[String] = ZIO.succeed("Hello!") } final class B { def bar: UIO[Int] = ZIO.succeed(42) } ``` When we call `ZIO.service[A]`, we are asking the ZIO environment for the `A` service. So then we can access all the functionality of the `A` service: ```scala val effect: ZIO[A, Nothing, String] = for { a <- ZIO.service[A] r <- a.foo } yield r ``` The signature of the above effect, says that in order to produce a value of type `String`, I need the `A` service from the ZIO environment. We can also use `ZIO.serviceWith`/`ZIO.serviceWithZIO` to directly access one of the service functionalities: ```scala object A { def foo: ZIO[A, Nothing, String] = ZIO.serviceWithZIO[A](_.foo) } object B { def bar: ZIO[B, Nothing, Int] = ZIO.serviceWithZIO[B](_.bar) } ``` ### Step 2: Writing Application Logic Using Services ZIO is a composable data type on its environment type parameter. So when we have an effect that requires the `A` service, and also we have another effect that requires the `B` service; when we compose these two services together, the resulting effect requires both `A` and `B` services: ```scala // Sequential Composition Example val myApp: ZIO[A with B, Nothing, (String, Int)] = for { a <- A.foo b <- B.bar } yield (a, b) ``` ```scala // Parallel Composition Example val myApp: ZIO[A with B, Nothing, (String, Int)] = A.foo <&> B.bar ``` Now the `myApp` effect requires `A` and `B` services to fulfill its functionality. We can see that we are writing application logic, we are not concerned about how services will be created! We are focused on using services to write the application logic. In the next step, we are going to build a dependency graph that holds two `A` and `B` services. ### Step 3: Building The Dependency Graph (Optional) To be able to run our application, we need to build the dependency graph that it needs. This can be done using the `ZLayer` data type. It allows us to build up the whole application's dependency graph by composing layers manually or automatically. Assume each of these services has its own layer like the below: ```scala object A { def foo: ZIO[A, Nothing, String] = ZIO.serviceWithZIO[A](_.foo) val layer: ZLayer[Any, Nothing, A] = ZLayer.succeed(new A) } object B { def bar: ZIO[B, Nothing, Int] = ZIO.serviceWithZIO[B](_.bar) val layer: ZLayer[Any, Nothing, B] = ZLayer.succeed(new B) } ``` In the previous example, the `myApp` application requires the `A` and `B` services. We can build that manually by composing two `A` and `B` layers horizontally: ```scala val appLayer: ZLayer[Any, Nothing, A with B] = A.layer ++ B.layer ``` Or we can use automatic layer construction: ```scala val appLayer: ZLayer[Any, Nothing, A with B] = ZLayer.make[A with B](A.layer, B.layer) ``` :::note Automatic layer construction is useful when the dependency graph is large and complex. So in simple cases, it doesn't demonstrate the power of automatic layer construction. ::: ### Step 4: Providing Dependencies to the ZIO Environment To run our application, we need to provide (inject) all dependencies to the ZIO environment. This can be done by using one of the `ZIO.provideXYZ` operations. This allows us to propagate dependencies from button to top: Let's provide our application with the `appLayer`: ```scala val result: ZIO[Any, Nothing, (String, Int)] = myApp.provideLayer(appLayer) ``` Here the `ZLayer` data types act as a dependency/environment eliminator. By providing required dependencies to our ZIO application, `ZLayer` eliminates all dependencies from the environment of our application. That's it! Now we can run our application: ```scala object MainApp extends ZIOAppDefault { def run = result } ``` Usually, when we use automatic layer construction, we skip the second step and instead provide all dependencies directly to the `ZIO.provide` operation. It takes care of building the dependency graph and providing the dependency graph to our ZIO application: ```scala object MainApp extends ZIOAppDefault { def run = myApp.provide(A.layer, B.layer) } ``` ## Dependency Injection and Service Pattern ### Dependency Injection When Writing Services When writing services, we might want to use other services. In such cases, we would like dependent services injected into our service. This is where we need to use dependency injection in order to write services. In ZIO, when we write services, we use class constructors to pass dependencies to the service. This is similar to the object-oriented style. For example, assume we have written the following `A` and `B` services: ```scala final class A { def foo: ZIO[Any, Nothing, String] = ZIO.succeed("Hello!") } object A { def foo: ZIO[A, Nothing, String] = ZIO.serviceWithZIO[A](_.foo) val layer: ZLayer[Any, Nothing, A] = ZLayer.succeed(new A) } final class B { def bar: ZIO[Any, Nothing, Int] = ZIO.succeed(42) } object B { def bar: ZIO[B, Nothing, Int] = ZIO.serviceWithZIO[B](_.bar) val layer: ZLayer[Any, Nothing, B] = ZLayer.succeed(new B) } ``` In order to write a service that depends on `A` and `B` services, we use the class constructor to pass dependencies to our service: ```scala final case class C(a: A, b: B) { def baz: ZIO[Any, Nothing, Unit] = for { _ <- a.foo _ <- b.bar } yield () } ``` To write a layer for our `C` service, we can use `ZIO.service` to access dependent services effectfully and pass them to the service's constructor: ```scala object C { def baz: ZIO[C, Nothing, Unit] = ZIO.serviceWithZIO[C](_.baz) val layer: ZLayer[A with B, Nothing, C] = ZLayer { for { a <- ZIO.service[A] b <- ZIO.service[B] } yield C(a, b) } } ``` Now, assume we have the following application logic: ```scala val myApp: ZIO[A with B with C, Nothing, Unit] = for { _ <- A.foo _ <- C.baz } yield () ``` In order to run the application, we should provide the `A`, `B` and `C` services: ```scala object MainApp extends ZIOAppDefault { def run = myApp.provide(A.layer, B.layer, C.layer) } ``` ### Dependency Injection When Writing Services Using Interfaces Although dependency injection is not about coding to the interface, it is a good pattern to have testable and configurable programs. When we code to the interface, our application logic will not dependent on any concrete implementation. So we can replace implementations, without changing the application. This is what [Service Pattern](../service-pattern/service-pattern.md) encourages us when writing services. Let's try an example. Assume we want to implement service `C` which is implemented in terms of `A` and `B` services. We want to keep our code modular and testable. The first step is to define interfaces for each service. This gives us the contract for how our services work together and lets us figure out our architecture and divide and conquer: ```scala trait A { def foo: ZIO[Any, Nothing, Int] } trait B { def bar: ZIO[Any, Nothing, String] } trait C { def baz: ZIO[Any, Nothing, Unit] } ``` The next step is to create implementations of our services taking their dependencies as constructor parameters. It's just constructor-based dependency injection: ```scala final case class ALive() extends A { def foo = ZIO.succeed(42) } final case class BLive() extends B { def bar: ZIO[Any, Nothing, String] = ZIO.succeed("Hello!") } final case class CLive(a: A, b: B) extends C { def baz: ZIO[Any, Nothing, Unit] = for { _ <- a.foo _ <- b.bar } yield () } ``` Now, we need to create layers for each of our implementations. This lets ZIO automatically wire them together. It also lets us take care of any setup or tear down. We use `ZIO.service` to grab things from the environment: ```scala modc:silent object ALive { val layer: ZLayer[Any, Nothing, ALive] = ZLayer.succeed(ALive()) } object BLive { val layer: ZLayer[Any, Nothing, BLive] = ZLayer.succeed(BLive()) } object CLive { val layer: ZLayer[B with A, Nothing, CLive] = ZLayer { for { a <- ZIO.service[A] b <- ZIO.service[B] } yield CLive(a, b) } } ``` Finally, it is time to write our application logic in terms of our services. We use `ZIO.service` once more in our main application to actually access the service that contains our main application logic and call it: ```scala val myApp: ZIO[A with C, Nothing, Unit] = for { a <- ZIO.service[A] _ <- a.foo c <- ZIO.service[C] _ <- c.baz } yield () ``` Now, in order to run our application, we wire all of our services together with `ZIO#provide` and inject them to our application: ```scala object MainApp extends ZIOAppDefault { def run = myApp.provide( ALive.layer, BLive.layer, CLive.layer ) } ``` For any purpose, if we decided to use another implementation for the `A` service, we can replace it easily without changing our application logic: ```scala final case class ACustom() extends A { def foo = ZIO.succeed(84) } object ACustom { val layer: ZLayer[Any, Nothing, A] = ZLayer.succeed(ACustom()) } object MainApp extends ZIOAppDefault { def run = myApp.provide( ACustom.layer, BLive.layer, CLive.layer ) } ``` ## Conclusion Following is a summary of some essential points when using dependency injection in ZIO: 1. For each service, we should write a layer that contains the recipe for creating the service. 2. We use class constructors to pass dependencies to our services. So inside the service, it is not idiomatic to use `ZIO.service` to access dependent services. 3. When writing a layer for a service that is dependent on other services, we use `ZIO.service` to access required services from the environment and then pass them to the service's constructor. 4. We use layers to compose and wire them together to create the dependency graph. --- ## Layers Are Shared by Default Layer memoization allows a layer to be created once and used multiple times in the dependency graph. So if we use the same layer twice, e.g. `(a >>> b) ++ (a >>> c)`, then the `a` layer will be allocated only once. ## Layers are Memoized by Default When Providing Globally One important feature of a ZIO application is that layers are shared by default, meaning that if the same layer is used twice, and if we provide the layer [globally](overriding-dependency-graph.md#global-environment) the layer will only be allocated a single time. For every layer in our dependency graph, there is only one instance of it that is shared between all the layers that depend on it. For example, assume we have the three `A`, `B`, and `C` services. The implementation of both `B` and `C` are dependent on the `A` service: ```scala trait A trait B trait C case class BLive(a: A) extends B case class CLive(a: A) extends C val a: ZLayer[Any, Nothing, A] = ZLayer(ZIO.succeed(new A {}).debug("initialized")) val b: ZLayer[A, Nothing, B] = ZLayer { for { a <- ZIO.service[A] } yield BLive(a) } val c: ZLayer[A, Nothing, C] = ZLayer { for { a <- ZIO.service[A] } yield CLive(a) } ``` Although both `b` and `c` layers require the `a` layer, the `a` layer is instantiated only once. It is shared with both `b` and `c`: ```scala object MainApp extends ZIOAppDefault { val myApp: ZIO[B & C, Nothing, Unit] = for { _ <- ZIO.service[B] _ <- ZIO.service[C] } yield () // alternative: myApp.provideLayer((a >>> b) ++ (a >>> c)) def run = myApp.provide(a, b, c) } // Output: // initialized: MainApp3$$anon$32@62c8b8d3 ``` ## Acquiring a Fresh Version If we don't want to share a module, we should create a fresh, non-shared version of it through `ZLayer#fresh`. ```scala object MainApp extends ZIOAppDefault { val myApp: ZIO[B & C, Nothing, Unit] = for { _ <- ZIO.service[B] _ <- ZIO.service[C] } yield () def run = myApp.provideLayer((a.fresh >>> b) ++ (a.fresh >>> c)) } // Output: // initialized: MainApp$$anon$22@7eb282da // initialized: MainApp$$anon$22@6397a26a ``` ## Layers Are Not Memoized When Providing Locally If we don't provide a layer globally but instead provide them [locally](overriding-dependency-graph.md#local-environment), that layer doesn't support memoization by default. In the following example, we provided the `A` layer two times locally and the ZIO doesn't memoize the construction of the `A` layer. So, it will be initialized two times: ```scala object MainApp extends ZIOAppDefault { val myApp: ZIO[Any, Nothing, Unit] = for { _ <- ZIO.service[A].provide(a) // providing locally _ <- ZIO.service[A].provide(a) // providing locally } yield () def run = myApp } // The output: // initialized: MainApp$$anon$1@cd60bde // initialized: MainApp$$anon$1@a984546 ``` ## Manual Memoization We can memoize the `A` layer manually using the `ZLayer#memoize` operator. It will return a scoped effect that, if evaluated, will return the lazily computed result of this layer: ```scala object MainApp extends ZIOAppDefault { val myApp: ZIO[Any, Nothing, Unit] = ZIO.scoped { a.memoize.flatMap { aLayer => for { _ <- ZIO.service[A].provide(aLayer) _ <- ZIO.service[A].provide(aLayer) } yield () } } def run = myApp } // The output: // initialized: MainApp$$anon$1@2bfc2bcc ``` --- ## Dependency Propagation When we write an application, our application has a lot of dependencies. We need a way to provide implementations and to feed and propagate all dependencies throughout the whole application. We can solve the propagation problem by using _ZIO environment_. During the development of an application, we don't care about implementations. Incrementally, when we use various effects with different requirements on their environment, all parts of our application compose together, and at the end of the day we have a ZIO effect which requires some services as an environment. Before running this effect by `unsafeRun` we should provide an implementation of these services into the ZIO Environment of that effect. ZIO has some facilities for doing this. `ZIO#provide` is the core function that allows us to _feed_ an `R` to an effect that requires an `R`. Notice that the act of `provide`ing an effect with its environment, eliminates the environment dependency in the resulting effect type, represented by type `Any` of the resulting environment. ## Using `ZIO#provideEnvironment` Method The `ZIO#provideEnvironment` takes an instance of `ZEnvironment[R]` and provides it to the `ZIO` effect which eliminates its dependency on `R`: ```scala trait ZIO[-R, +E, +A] { def provideEnvironment(r: => ZEnvironment[R]): IO[E, A] } ``` This is similar to dependency injection, and the `provide*` function can be thought of as _inject_. Assume we have the following services: ```scala trait EmailService { def send(email: String, content: String): UIO[Unit] } object EmailService { def send(email: String, content: String) = ZIO.serviceWithZIO[EmailService](_.send(email, content)) } ``` Let's write a simple program using `EmailService` service: ```scala val app: ZIO[EmailService, Nothing, Unit] = EmailService.send("john@doe.com", "Hello John!") ``` We can `provide` implementation of `EmailService` service into the `app` effect: ```scala val loggingImpl = new EmailService { override def send(email: String, content: String): UIO[Unit] = ??? } val effect = app.provideEnvironment(ZEnvironment(loggingImpl)) ``` Most of the time, we don't use `ZIO#provideEnvironment` directly to provide our services; instead, we use `ZLayer` to construct the dependency graph of our application, then we use methods like `ZIO#provide`, `ZIO#provideSome`, `ZIO#provideSomeAuto` and `ZIO#provideCustom` to propagate dependencies into the environment of our ZIO effect. ## Using `ZIO#provide` Method Unlike the `ZIO#provideEnvironment` which takes a `ZEnvironment[R]`, the `ZIO#provide` takes a `ZLayer` to the ZIO effect and translates it to another level. Assume we have written this piece of program that requires `Foo` and `Bar` services: ```scala trait Foo { def foo(): UIO[String] } object Foo { def foo(): ZIO[Foo, Nothing, String] = ZIO.serviceWithZIO[Foo](_.foo()) } case class FooLive() extends Foo { override def foo(): UIO[String] = ZIO.succeed("foo") } object FooLive { val layer: ULayer[Foo] = ZLayer.succeed(FooLive()) } trait Bar { def bar(): UIO[Int] } object Bar { def bar(): ZIO[Bar, Nothing, Int] = ZIO.serviceWithZIO[Bar](_.bar()) } case class BarLive() extends Bar { override def bar(): UIO[Int] = ZIO.succeed(1) } object BarLive { val layer: ULayer[Bar] = ZLayer.succeed(BarLive()) } val myApp: ZIO[Foo & Bar, Nothing, Unit] = for { foo <- Foo.foo() bar <- Bar.bar() _ <- ZIO.debug(s"foo: $foo, bar: $bar") } yield () ``` We provide implementations of `Foo`, `Bar` services to the `myApp` effect by using `ZIO#provide` method: ```scala val mainEffect: ZIO[Any, Nothing, Unit] = myApp.provide(FooLive.layer, BarLive.layer) ``` As we see, the type of our effect converted from `ZIO[Foo & Bar, Nothing, Unit]` which requires two services to `ZIO[Any, Nothing, Unit]` effect which doesn't require any services. ## Using `ZIO#provideSome` Method Sometimes we have written a program, and we don't want to provide all its requirements. In these cases, we can use `ZIO#provideSome` to partially apply some layers to the `ZIO` effect. In the previous example, if we just want to provide the `Foo`, we should use `ZIO#provideSome`: ```scala val mainEffectSome: ZIO[Bar, Nothing, Unit] = myApp.provideSome(FooLive.layer) ``` :::caution When using `ZIO#provideSome[R0]`, we should provide the remaining type as `R0` type parameter. This workaround helps the compiler to infer the proper types. ::: ## Using `ZIO#provideSomeAuto` Method In Scala 3 enhanced version of `ZIO#provideSome` is introduced. The `ZIO#provideSomeAuto` method automatically infers the remaining type of the effect. ```scala val mainEffectSomeAuto = myApp.provideSomeAuto(FooLive.layer) // No need to provide `Bar` anywhere ``` --- ## Examples ## An Example of a ZIO Application with Multiple Config Layers In the following example, we have an application that requires `AppConfig` layer, which itself requires `DBConfig` and `ServerConfig` layers: ```scala case class ServerConfig(host: String, port: Int) object ServerConfig { val layer: ULayer[ServerConfig] = ZLayer.succeed(ServerConfig("localhost", 8080)) } case class DBConfig(name: String) object DBConfig { val layer: ULayer[DBConfig] = ZLayer.succeed(DBConfig("my-test-db")) } case class AppConfig(db: DBConfig, serverConfig: ServerConfig) object AppConfig { val layer: ZLayer[DBConfig with ServerConfig, Nothing, AppConfig] = ZLayer { for { db <- ZIO.service[DBConfig] server <- ZIO.service[ServerConfig] } yield AppConfig(db, server) } } object MainApp extends ZIOAppDefault { val myApp = for { c <- ZIO.service[AppConfig] _ <- ZIO.debug(s"Application started with config: ${c}") } yield () def run = myApp.provide(AppConfig.layer, DBConfig.layer, ServerConfig.layer) } ``` ## An Example of Manually Generating a Dependency Graph Suppose we have defined the `UserRepo`, `DocumentRepo`, `Database`, `BlobStorage`, and `Cache` services and their respective implementations as follows: ```scala case class User(email: String, name: String) trait UserRepo { def save(user: User): Task[Unit] def get(email: String): Task[User] } object UserRepo { def save(user: User): ZIO[UserRepo, Throwable, Unit] = ZIO.serviceWithZIO(_.save(user)) def get(email: String): ZIO[UserRepo, Throwable, User] = ZIO.serviceWithZIO(_.get(email)) } case class UserRepoLive(cache: Cache, database: Database) extends UserRepo { override def save(user: User): Task[Unit] = ??? override def get(email: String): Task[User] = ??? } object UserRepoLive { val layer: URLayer[Cache & Database, UserRepo] = ZLayer { for { cache <- ZIO.service[Cache] database <- ZIO.service[Database] } yield UserRepoLive(cache, database) } } trait Database case class DatabaseLive() extends Database object DatabaseLive { val layer: ZLayer[Any, Nothing, Database] = ZLayer.succeed(DatabaseLive()) } trait Cache { def save(key: String, value: Array[Byte]): Task[Unit] def get(key: String): Task[Array[Byte]] def remove(key: String): Task[Unit] } class InmemeoryCache() extends Cache { override def save(key: String, value: Array[Byte]): Task[Unit] = ??? override def get(key: String): Task[Array[Byte]] = ??? override def remove(key: String): Task[Unit] = ??? } object InmemoryCache { val layer: ZLayer[Any, Throwable, Cache] = ZLayer(ZIO.attempt(new InmemeoryCache).debug("initialized")) } class PersistentCache() extends Cache { override def save(key: String, value: Array[Byte]): Task[Unit] = ??? override def get(key: String): Task[Array[Byte]] = ??? override def remove(key: String): Task[Unit] = ??? } object PersistentCache { val layer: ZLayer[Any, Throwable, Cache] = ZLayer(ZIO.attempt(new PersistentCache).debug("initialized")) } case class Document(title: String, author: String, body: String) trait DocumentRepo { def save(document: Document): Task[Unit] def get(id: String): Task[Document] } object DocumentRepo { def save(document: Document): ZIO[DocumentRepo, Throwable, Unit] = ZIO.serviceWithZIO(_.save(document)) def get(id: String): ZIO[DocumentRepo, Throwable, Document] = ZIO.serviceWithZIO(_.get(id)) } case class DocumentRepoLive(cache: Cache, blobStorage: BlobStorage) extends DocumentRepo { override def save(document: Document): Task[Unit] = ??? override def get(id: String): Task[Document] = ??? } object DocumentRepoLive { val layer: ZLayer[Cache & BlobStorage, Nothing, DocumentRepo] = ZLayer { for { cache <- ZIO.service[Cache] blobStorage <- ZIO.service[BlobStorage] } yield DocumentRepoLive(cache, blobStorage) } } trait BlobStorage { def store(key: String, value: Array[Byte]): Task[Unit] } case class BlobStorageLive() extends BlobStorage { override def store(key: String, value: Array[Byte]): Task[Unit] = ??? } object BlobStorageLive { val layer: URLayer[Any, BlobStorage] = ZLayer.succeed(BlobStorageLive()) } ``` And then assume we have the following ZIO application: ```scala def myApp: ZIO[DocumentRepo & UserRepo, Throwable, Unit] = for { _ <- UserRepo.save(User("john@doe", "john")) _ <- DocumentRepo.save(Document("introduction to zio", "john", "")) _ <- UserRepo.get("john@doe").debug("retrieved john@doe user") _ <- DocumentRepo.get("introduction to zio").debug("retrieved article about zio") } yield () ``` The `myApp` requires `DocumentRepo` and `UserRepo` services to run. So we need to create a `ZLayer` which requires no services and produces `DocumentRepo` and `UserRepo`. We can manually create this layer using [vertical and horizontal layer composition](manual-layer-construction.md#vertical-and-horizontal-composition): ```scala object MainApp extends ZIOAppDefault { val layers: ZLayer[Any, Any, DocumentRepo with UserRepo] = (BlobStorageLive.layer ++ InmemoryCache.layer ++ DatabaseLive.layer) >>> (DocumentRepoLive.layer >+> UserRepoLive.layer) def run = myApp.provideLayer(layers) } ``` ## An Example of Automatically Generating a Dependency Graph Instead of creating the required layer manually, we can use the `ZIO#provide`. ZIO internally creates the dependency graph automatically based on all dependencies provided: ```scala object MainApp extends ZIOAppDefault { def run = myApp.provide( InmemoryCache.layer, DatabaseLive.layer, UserRepoLive.layer, BlobStorageLive.layer, DocumentRepoLive.layer ) } ``` ## An Example of Providing Different Implementations of the Same Service Let's say we want to provide different versions of the same service to different services. In this example, both `UserRepo` and `DocumentRepo` services require the `Cache` service. However, we want to provide different cache implementations for these two services. Our goal is to provide an `InmemoryCache` layer for `UserRepo` and a `PersistentCache` layer for the `DocumentRepo` service: ```scala object MainApp extends ZIOAppDefault { val layers: ZLayer[Any, Throwable, UserRepo with DocumentRepo] = ((InmemoryCache.layer ++ DatabaseLive.layer) >>> UserRepoLive.layer) ++ ((PersistentCache.layer ++ BlobStorageLive.layer) >>> DocumentRepoLive.layer) def run = myApp.provideLayer(layers) } // Output: // initialized: zio.examples.PersistentCache@6e899128 // initialized: zio.examples.InmemeoryCache@852e20a ``` ## An Example of How to Get Fresh Layers Having covered the topic of [acquiring fresh layers](../../reference/di/dependency-memoization.md#acquiring-a-fresh-version), let's see an example of using the `ZLayer#fresh` operator. `DocumentRepo` and `UserRepo` services are dependent on an in-memory cache service. On the other hand, let's assume the cache service is quite simple, and we might be prone to cache conflicts between services. While sharing the cache service may cause some problems for our business logic, we should separate the cache service for both `DocumentRepo` and `UserRepo`: ```scala object MainApp extends ZIOAppDefault { val layers: ZLayer[Any, Throwable, UserRepo & DocumentRepo] = ((InmemoryCache.layer.fresh ++ DatabaseLive.layer) >>> UserRepoLive.layer) ++ ((InmemoryCache.layer.fresh ++ BlobStorageLive.layer) >>> DocumentRepoLive.layer) def run = myApp.provideLayer(layers) } // Output: // initialized: zio.examples.InmemoryCache@13c9672b // initialized: zio.examples.InmemoryCache@26d79027 ``` ## An Example of Pass-through Dependencies Notice that in the previous examples, both `UserRepo` and `DocuemntRepo` have some [hidden dependencies](../../reference/di/manual-layer-construction.md#hidden-versus-passed-through-dependencies), such as `Cache`, `Database`, and `BlobStorage`. So these hidden dependencies are no longer expressed in the type signature of the `layers`. From the perspective of a caller, `layers` just outputs a `UserRepo` and `DocuemntRepo` and requires no inputs. The caller does not need to be concerned with the internal implementation details of how the `UserRepo` and `DocumentRepo` are constructed. An upstream dependency that is used by many other services can be "passed-through" and included in a layer's output. This can be done with the `>+>` operator, which provides the output of one layer to another layer, returning a new layer that outputs the services of _both_. The following example shows how to passthrough all dependencies to the final layer: ```scala object MainApp extends ZIOAppDefault { // passthrough all dependencies val layers: ZLayer[Any, Throwable, Database & BlobStorage & Cache & DocumentRepo & UserRepo] = DatabaseLive.layer >+> BlobStorageLive.layer >+> InmemoryCache.layer >+> DocumentRepoLive.layer >+> UserRepoLive.layer // providing all passthrough dependencies to the ZIO application def run = myApp.provideLayer(layers) } ``` ## An Example of Updating Hidden Dependencies One of the use cases of having explicit all dependencies in the final layer is that we can [update](../../reference/di/examples.md#an-example-of-updating-hidden-dependencies) those hidden layers using `ZLayer#update`. In the following example, we are replacing the `InmemoryCache` with another implementation called `PersistentCache`: ```scala object MainApp extends ZIOAppDefault { def myApp: ZIO[DocumentRepo & UserRepo, Nothing, Unit] = for { _ <- ZIO.service[UserRepo] _ <- ZIO.service[DocumentRepo] } yield () val layers: ZLayer[Any, Throwable, Database & BlobStorage & Cache & DocumentRepo & UserRepo] = DatabaseLive.layer >+> BlobStorageLive.layer >+> InmemoryCache.layer >+> DocumentRepoLive.layer >+> UserRepoLive.layer def run = myApp.provideLayer( layers.update[Cache](_ => new PersistentCache) ) } ``` --- ## Introduction to Dependency Injection in ZIO ## What is a Dependency? When we implement a service, we might need to use other services. So a dependency is just another service that is required to fulfill its functionality: ```scala class Editor { val formatter = new Formatter val compiler = new Compiler def formatAndCompile(code: String): UIO[String] = formatter.format(code).flatMap(compiler.compile) } ``` ## What is Dependency Injection? Dependency injection is a pattern for decoupling the usage of dependencies from their actual creation process. In other words, it is a process of injecting dependencies of service from the outside world. The service itself doesn't know how to create its dependencies. The following example shows an `Editor` service that depends on `Formatter` and `Compiler` services. It doesn't use dependency injection: ```scala class Editor { private val formatter = new Formatter private val compiler = new Compiler def formatAndCompile(code: String): UIO[String] = formatter.format(code).flatMap(compiler.compile) } ``` The `Editor` class in the above example is responsible for creating the `Formatter` and `Compiler` services. The client of the `Editor` class doesn't have any control over these services. The client can't use a different implementation for the `Formatter` and `Compiler` services. So it makes it hard to test the `Editor` class. Let's try to change the above example to use the constructor-based dependency injection pattern: ```scala class Editor(formatter: Formatter, compiler: Compiler) { def formatAndCompile(code: String): UIO[String] = ??? } ``` In this example, the `Editor` service is not responsible for creating its dependencies. Instead, they are expected to be injected from the caller site. The `Editor` service does not know how its dependencies are created, they are just injected into its constructor. So dependency injection is a very simple concept and can be implemented with simple constructs. In a lot of situations, we are not required to use any tools or frameworks. In the [motivation page](motivation.md) we explain why applications should use the dependency injection pattern in more detail. ## ZIO's Built-in Dependency Injection ZIO has a full solution to the dependency injection problem. It provides a built-in approach to dependency injection using the following tools in combination together: 1. **ZIO Environment** 1. We use the `ZIO.serviceXYZ` to access services inside the ZIO environment, without having any knowledge of how the services are created or implemented. Using `ZIO.serviceXYZ` helps us to decouple our usage of services from the implementation of the services. Consequently, all dependencies will be encoded inside the `R` type parameter of our ZIO application. This specifies which services are required to fulfill the application's functionality. 2. We use the `ZIO.provideXYZ` to provide services to the ZIO environment. This is the opposite operation of `ZIO.serviceXYZ`. It allows us to inject all dependencies into the ZIO environment. 2. **ZLayer**— We use layers to create the dependency graph that our application depends on. We can have dependency injection through three simple steps: 1. Accessing services from the ZIO environment 2. Building the dependency graph 3. Providing services to the ZIO environment We will discuss them in more detail throughout [this page](dependency-injection-in-zio.md). ## ZIO's Dependency Injection Features Dependency injection in ZIO is very powerful, which increases developer productivity. Let's recap some important features of dependency injection in ZIO: 1. **Composable** 1. **Composable Environment**— Because of the very composable nature of the `ZIO` data type, its environment type parameter is also composable. So when we compose multiple `ZIO` effects, where each one requires a specific service, we finally get a `ZIO` effect that requires all the required services that each of the composed effects requires. For example, if we `zip` two effects of type `ZIO[A, Nothing, Int]` and `ZIO[B, Throwable, String]`, the result of this operation will become `ZIO[A with B, Throwable, (Int, String)]`. The result operation requires both `A` and `B` services. 2. **Composable Dependencies**— The `ZLayer` is also composable, as well as ZIO's environment type parameter. So we can compose multiple layers to [create a complex dependency graph](building-dependency-graph.md). 2. **Type-Safe**— All the required dependencies should be provided at compile time. If we forget to provide the required services at compile time, we will get a compile error. So if our program compiles successfully, we can be sure that we won't have runtime errors due to missing dependencies. 3. **Effectful**— We build dependency graphs using `ZLayer`. Since `ZLayer` is effectful, we can create a dependency graph in an effectful way. 4. **Resourceful**— It also helps us to have resourceful dependencies, where we can manage the creation and release phases of the dependencies. 5. **Parallelism**— All dependencies are created in parallel, and will be provided to our application. ## Other Frameworks Using `ZLayer` along with the ZIO environment to use dependency injection is optional. While we encourage users to use ZIO's idiomatic dependency injection, it is not mandatory. We can still use other DI solutions. Here are some other options: - [Guice](https://github.com/google/guice) - [izumi distage](https://izumi.7mind.io/distage/index.html) - [MacWire](https://github.com/softwaremill/macwire) --- ## Manual Layer Construction We said that we can think of the `ZLayer` as a more powerful _constructor_. Constructors are not composable, because they are not values. While a constructor is not composable, `ZLayer` has a nice facility to compose with other `ZLayer`s. So we can say that a `ZLayer` turns a constructor into values. :::note In a regular ZIO application we are not required to build the dependency graph through composing layers tougher. Instead, we can provide all dependencies to the ZIO application using `ZIO#provide`, and the ZIO will create the dependency graph manually under the hood. Therefore, use manual layer composition if you know what you're doing. ::: ## Vertical and Horizontal Composition Assume we have several services with their dependencies, and we need a way to compose and wire up these dependencies to create the dependency graph of our application. `ZLayer` is a ZIO solution for this problem, it allows us to build up the whole application dependency graph by composing layers horizontally and vertically. ### Horizontal Composition Layers can be composed together horizontally with the `++` operator. When we compose layers horizontally, the new layer requires all the services that both of them require and produces all services that both of them produce. Horizontal composition is a way of composing two layers side-by-side. It is useful when we combine two layers that don't have any relationship with each other. We can compose `fooLayer` and `barLayer` _horizontally_ to build a layer that has the requirements of both, to provide the capabilities of both, through `fooLayer ++ barLayer`: ```scala val fooLayer: ZLayer[A, Throwable, B] = ??? // A ==> B val barLayer: ZLayer[C, Nothing , D] = ??? // C ==> D val horizontal: ZLayer[A & C, Throwable, B & D] = // A & C ==> B & D fooLayer ++ barLayer ``` ### Vertical Composition We can also compose layers _vertically_ using the `>>>` operator, meaning the output of one layer is used as input for the subsequent layer, resulting in one layer with the requirement of the first, and the output of the second. For example if we have a layer that requires `A` and produces `B`, we can compose this with another layer that requires `B` and produces `C`; this composition produces a layer that requires `A` and produces `C`. The feed operator, `>>>`, stack them on top of each other by using vertical composition. This sort of composition is like _function composition_, feeding an output of one layer to an input of another: ```scala val fooLayer: ZLayer[A, Throwable, B] = ??? // A ==> B val barLayer: ZLayer[B, Nothing , C] = ??? // B ==> C val horizontal: ZLayer[A, Throwable, C] = // A ==> C fooLayer >>> barLayer ``` ## Hidden Versus Passed-through Dependencies ZLayer has a `passthrough` operator which returns a new layer that produces the outputs of this layer but also passes-through the inputs: ```scala val fooLayer: ZLayer[A, Nothing, B] = ??? // A ==> B val result1 : ZLayer[A, Nothing, A & B] = // A ==> A & B fooLayer.passthrough val result2 : ZLayer[A, Nothing, A & B] = // A ==> A & B ZLayer.service[A] ++ fooLayer // (A ==> A) ++ (A ==> B) // (A ==> A & B) ``` By default, the `ZLayer` hides intermediate dependencies when composing vertically. For example, when we compose `fooLayer` with `barLayer` vertically, the output would be a `ZLayer[A, Throwable, C]`. This hides the dependency on the `B` layer. By using the above technique, we can pass through hidden dependencies. Let's include the `B` service into the upstream dependencies of the final layer using the `ZIO.service[B]`. We can think of `ZIO.service[B]` as an _identity function_ (`B ==> B`). ```scala val fooLayer: ZLayer[A, Throwable, B] = ??? // A ==> B val barLayer: ZLayer[B, Throwable, C] = ??? // B ==> C val finalLayer: ZLayer[A & B, Throwable, C] = // A & B ==> C (fooLayer ++ ZLayer.service[B]) >>> barLayer // ((A ==> B) ++ (B ==> B)) >>> (B ==> C) // (A & B ==> B) >> (B ==> C) // (A & B ==> C) ``` Or we may want to include the middle services in the output channel of the final layer, resulting in a new layer with the inputs of the first layer and the outputs of both layers: ```scala val fooLayer: ZLayer[A, Throwable, B] = ??? // A ==> B val barLayer: ZLayer[B, Throwable, C] = ??? // B ==> C val finalLayer: ZLayer[A, Throwable, B & C] = // A ==> B & C fooLayer >>> (ZLayer.service[B] ++ barLayer) // (A ==> B) >>> ((B ==> B) ++ (B ==> C)) // (A ==> B) >>> (B ==> B & C) // (A ==> B & C) ``` We can do the same with the `>+>` operator: ```scala val fooLayer: ZLayer[A, Throwable, B] = ??? // A ==> B val barLayer: ZLayer[B, Throwable, C] = ??? // B ==> C val finalLayer: ZLayer[A, Throwable, B & C] = // A ==> B & C fooLayer >+> barLayer ``` This technique is useful when we want to defer the creation of some intermediate services and require them as part of the input of the final layer. For example, assume we have these two layers: ```scala val fooLayer: ZLayer[A , Throwable, B] = ??? // A ==> B val barLayer: ZLayer[B & C, Throwable, D] = ??? // B & C ==> D val finalLayer: ZLayer[A & C, Throwable, D] = // A & C ==> B & D fooLayer >>> barLayer ``` So we can defer the creation of the `C` layer using `ZLayer.service[C]`: ```scala val fooLayer: ZLayer[A , Throwable, B] = ??? // A ==> B val barLayer: ZLayer[B & C, Throwable, D] = ??? // B & C ==> D val layer: ZLayer[A & C, Throwable, D] = // A & C ==> D (fooLayer ++ ZLayer.service[C]) >>> barLayer // ((A ==> B) ++ (C ==> C)) >>> (B & C ==> D) // (A & C ==> B & C) >>> (B & C ==> D) // (A & C ==> D) ``` Here is an example in which we passthrough all requirements to bake a `Cake` so all the requirements are available to all the downstream services: ```scala trait Baker trait Ingredients trait Oven trait Dough trait Cake lazy val baker : ZLayer[Any, Nothing, Baker] = ??? lazy val ingredients: ZLayer[Any, Nothing, Ingredients] = ??? lazy val oven : ZLayer[Any, Nothing, Oven] = ??? lazy val dough : ZLayer[Baker & Ingredients, Nothing, Dough] = ??? lazy val cake : ZLayer[Baker & Oven & Dough, Nothing, Cake] = ??? lazy val all: ZLayer[Any, Nothing, Baker & Ingredients & Oven & Dough & Cake] = baker >+> // Baker ingredients >+> // Baker & Ingredients oven >+> // Baker & Ingredients & Oven dough >+> // Baker & Ingredients & Oven & Dough cake // Baker & Ingredients & Oven & Dough & Cake ``` This allows a style of composition where the `>+>` operator is used to build a progressively larger set of services, with each new service able to depend on all the services before it. If we passthrough dependencies and later want to hide them we can do so through a simple type ascription: ```scala lazy val hidden: ZLayer[Any, Nothing, Cake] = all ``` The `ZLayer` makes it easy to mix and match these styles. If we build our dependency graph more explicitly, we can be confident that dependencies used in multiple parts of the dependency graph will only be created once due to memoization and sharing. Using these simple operators we can build complex dependency graphs. ## Updating Local Dependencies Given a layer, it is possible to update one or more components it provides. We update a dependency in two ways: 1. **Using the `update` Method** — This method allows us to replace one requirement with a different implementation: ```scala val origin: ZLayer[Any, Nothing, String & Int & Double] = ZLayer.succeedEnvironment(ZEnvironment[String, Int, Double]("foo", 123, 1.3)) val updated1 = origin.update[String](_ + "bar") val updated2 = origin.update[Int](_ + 5) val updated3 = origin.update[Double](_ - 0.3) ``` Here is an example of updating a config layer: ```scala case class AppConfig(poolSize: Int) object MainApp extends ZIOAppDefault { val myApp: ZIO[AppConfig, IOException, Unit] = for { config <- ZIO.service[AppConfig] _ <- Console.printLine(s"Application config after the update operation: $config") } yield () val appLayers: ZLayer[Any, Nothing, AppConfig] = ZLayer(ZIO.succeed(AppConfig(5)).debug("Application config initialized")) val updatedConfig: ZLayer[Any, Nothing, AppConfig] = appLayers.update[AppConfig](c => c.copy(poolSize = c.poolSize + 10) ) def run = myApp.provide(updatedConfig) } // Output: // Application config initialized: AppConfig(5) // Application config after the update operation: AppConfig(15) ``` 2. **Using Horizontal Composition** — Another way to update a requirement is to horizontally compose in a layer that provides the updated service. The resulting composition will replace the old layer with the new one: ```scala val origin: ZLayer[Any, Nothing, String & Int & Double] = ZLayer.succeedEnvironment(ZEnvironment[String, Int, Double]("foo", 123, 1.3)) val updated = origin ++ ZLayer.succeed(321) ``` Let's see an example of updating a config layer: ```scala case class AppConfig(poolSize: Int) object MainApp extends ZIOAppDefault { val myApp: ZIO[AppConfig, IOException, Unit] = for { config <- ZIO.service[AppConfig] _ <- Console.printLine(s"Application config after the update operation: $config") } yield () val appLayers: ZLayer[Any, Nothing, AppConfig] = ZLayer(ZIO.succeed(AppConfig(5)).debug("Application config initialized")) val updatedConfig: ZLayer[Any, Nothing, AppConfig] = appLayers ++ ZLayer.succeed(AppConfig(8)) def run = myApp.provide(updatedConfig) } // Output: // Application config initialized: AppConfig(5) // Application config after the update operation: AppConfig(8) ``` ## Cyclic Dependencies The `ZLayer` mechanism makes it impossible to build cyclic dependencies, making the initialization process very linear, by construction. --- ## Motivation :::caution In this section, we are going to study how ZIO supports dependency injection by providing pedagogical examples. Examples provided in these sections are not idiomatic and are not meant to be used as a reference. We will discuss the idiomatic way to use dependency injection in ZIO later. So feel free to skip reading this section if you are not interested to learn the underlying concepts in detail. ::: Assume we have two services called `Formatter` and `Compiler` like the below: ```scala class Formatter { def format(code: String): UIO[String] = ZIO.succeed(code) // dummy implementation } class Compiler { def compile(code: String): UIO[String] = ZIO.succeed(code) // dummy implementation } ``` We want to create an editor service, which uses these two services. Hence, we are going to instantiate the required services inside the `Editor` class: ```scala class Editor { private val formatter: Formatter = new Formatter() private val compiler: Compiler = new Compiler() def formatAndCompile(code: String): UIO[String] = formatter.format(code).flatMap(compiler.compile) } ``` There are some problems with this approach: 1. Users of the `Editor` service haven't any control over how dependencies will be created. 2. Users of the `Editor` service cannot use different implementations of `Formatter` and `Compiler` services. For example, we would like to test the `Editor` service with a mock version of `Formatter` and `Compiler`. With this approach, mocking these dependencies is hard. 3. The `Editor` service is tightly coupled with `Formatter` and `Compiler`. This means any change to these services, may introduce a new change in the `Editor` class. 4. Creating the object graph is a manual process. Let's see how we can provide a solution to these problems. In the following sections, we will step by step solve these problems, and finally, we will see how ZIO solves the dependency injection problem. ## Step 1: Inversion of Control On solution to the first problem is inverting the control to the user of the `Editor` service, which is called _Inversion of Control_. Now lets instead of instantiating the dependencies inside the `Editor` service, create them outside the `Editor` service and pass them to the `Editor` service: ```scala class Editor(formatter: Formatter, compiler: Compiler) { def formatAndCompile(code: String): UIO[String] = formatter.format(code).flatMap(compiler.compile) } ``` Now the `Editor` service is decoupled from how the `Formatter` and `Compiler` services are created. The client of the `Editor` service can instantiate the `Formatter` and `Compiler` services and pass them to the `Editor` service: ```scala val formatter = new Formatter() // creating formatter val compiler = new Compiler() // creating compiler val editor = new Editor(formatter, compiler) // assembling formatter and compiler into editor editor.formatAndCompile("println(\"Hello, world!\")") ``` ## Step 2: Decoupling from Implementations In the previous step, we delegated the creation of dependencies to the client of the `Editor` service. This decouples the `Editor` service from the creation of the dependencies. But it is not enough. We still coupled to the concrete classes called `Formatter` and `Compiler`. The user of the `Editor` service cannot use different implementations rather than the `Formatter` and `Compiler` services. This is where the object-oriented approach comes into play. By programming to interfaces, we can encapsulate the `Editor` service and make it independent of concrete implementations: ```scala trait Formatter { def format(code: String): UIO[String] } class ScalaFormatter extends Formatter { def format(code: String): UIO[String] = ZIO.succeed(code) // dummy implementation } trait Compiler { def compile(code: String): UIO[String] } class ScalaCompiler extends Compiler { def compile(code: String): UIO[String] = ZIO.succeed(code) // dummy implementation } trait Editor { def formatAndCompile(code: String): UIO[String] } class EditorLive(formatter: Formatter, compiler: Compiler) extends Editor { def formatAndCompile(code: String): UIO[String] = formatter.format(code).flatMap(compiler.compile) } val formatter = new ScalaFormatter() // Creating Formatter val compiler = new ScalaCompiler() // Creating Compiler val editor = new EditorLive(formatter, compiler) // Assembling formatter and compiler into CodeEditor editor.formatAndCompile("println(\"Hello, world!\")") ``` Now, we can test the `Editor` service easily without having to worry about the implementation of the `Formatter` and `Compiler` services. To test the `Editor` service, we can use a mock implementation of its dependencies: ```scala class MockFormatter extends Formatter { def format(code: String): UIO[String] = ZIO.succeed(code) // dummy implementation } class MockCompiler extends Compiler { def compile(code: String): UIO[String] = ZIO.succeed(code) // dummy implementation } val formatter = new MockFormatter() // Creating mock formatter val compiler = new MockCompiler() // Creating mock compiler val editor = new EditorLive(formatter, compiler) // Assembling formatter and compiler into CodeEditor val expectedOutput = ??? for { r <- editor.formatAndCompile("println(\"Hello, world!\")") } yield assertTrue(r == expectedOutput) ``` ## Step 3: Binding Interfaces to their Implementations In the previous step, we successfully decoupled the `Editor` service from concrete dependencies. However, there is still a problem. When the application grows, the number of dependencies might increase. So, instead of injecting the dependencies manually whenever needed, we would like to maintain a mapping from interfaces to their implementations in a container, and then whenever needed, we can ask for the required dependency from the container. So we need a container that maintains this mapping. ZIO has a type-level map, called `ZEnvironment`, which can do that for us: ```scala val scalaFormatter = new ScalaFormatter() // Creating Formatter val scalaCompiler = new ScalaCompiler() // Creating Compiler val myEditor = // Assembling Formatter and Compiler into an Editor new EditorLive( scalaFormatter, scalaCompiler ) val environment = ZEnvironment[Formatter, Compiler, Editor](scalaFormatter, scalaCompiler, myEditor) // Map( // Formatter -> scalaFormatter, // Compiler -> scalaCompiler // Editor -> myEditor //) ``` Now, whenever we need an object of type `Formatter`, `Compiler`, or `Editor`, we can ask the `environment` for them. ```scala object MainApp extends ZIOAppDefault { def run = environment.get[Editor].formatAndCompile("println(\"Hello, world!\")") } ``` Here is another example: ```scala val workflow: ZIO[Any, Nothing, Unit] = for { f <- environment.get[Formatter].format("println(\"Hello, world!\")") _ <- environment.get[Compiler].compile(f) } yield () ``` ## Step 4: Effectful Constructors Until now, we discussed the creation of services where the creation process was not effectful. But, assume in order to implement the `Editor` service, we need the `Counter` service, and the creation of `Counter` itself is effectful: ```scala trait Counter { def inc: UIO[Unit] def dec: UIO[Unit] def get: UIO[Int] } case class CounterLive(ref: Ref[Int]) extends Counter { def inc: UIO[Unit] = ref.update(_ + 1) def dec: UIO[Unit] = ref.update(_ - 1) def get: UIO[Int] = ref.get } object CounterLive { // Effectful constructor def make: UIO[Counter] = Ref.make(0).map(new CounterLive(_)) } class EditorLive( formatter: Formatter, compiler: Compiler, counter: Counter ) extends Editor { def formatAndCompile(code: String): UIO[String] = ??? } ``` To instantiate `EditorLive` we can't use the same technique as before: ```scala val scalaFormatter = new ScalaFormatter() // Creating Formatter val scalaCompiler = new ScalaCompiler() // Creating Compiler val myEditor = // Assembling Formatter and Compiler into an Editor new EditorLive( scalaFormatter, scalaCompiler, CounterLive.make // Compiler Error: Type mismatch: expected: Counter, found: UIO[Counter] ) ``` We can use `ZIO#flatMap` to create the dependency graph but to make it easier, we have a special data type called `ZLayer`. It is effectful, so we can use it to create the dependency graph effectfully: ```scala trait Formatter { def format(code: String): UIO[String] } case class ScalaFormatter() extends Formatter { def format(code: String): UIO[String] = ZIO.succeed(code) // dummy implementation } object ScalaFormatter { val layer: ULayer[Formatter] = ZLayer.succeed(ScalaFormatter()) } trait Compiler { def compile(code: String): UIO[String] } case class ScalaCompiler() extends Compiler { def compile(code: String): UIO[String] = ZIO.succeed(code) } object ScalaCompiler { val layer = ZLayer.succeed(ScalaCompiler()) } trait Editor { def formatAndCompile(code: String): UIO[String] } trait Counter { def inc: UIO[Unit] def dec: UIO[Unit] def get: UIO[Int] } case class CounterLive(ref: Ref[Int]) extends Counter { def inc: UIO[Unit] = ref.update(_ + 1) def dec: UIO[Unit] = ref.update(_ - 1) def get: UIO[Int] = ref.get } object CounterLive { // Effectful constructor def make: UIO[Counter] = Ref.make(0).map(new CounterLive(_)) val layer: ULayer[Counter] = ZLayer.fromZIO(CounterLive.make) } case class EditorLive( formatter: Formatter, compiler: Compiler, counter: Counter ) extends Editor { def formatAndCompile(code: String): UIO[String] = ??? } object EditorLive { val layer: ZLayer[Counter with Compiler with Formatter, Nothing, Editor] = ZLayer { for { // we will discuss ZIO.service later formatter <- ZIO.service[Formatter] compiler <- ZIO.service[Compiler] counter <- ZIO.service[Counter] } yield EditorLive(formatter, compiler, counter) } } object MainApp extends ZIOAppDefault { val environment = ((ScalaFormatter.layer ++ ScalaCompiler.layer ++ CounterLive.layer) >>> EditorLive.layer).build def run = for { editor <- environment.map(_.get[Editor]) _ <- editor.formatAndCompile("println(\"Hello, world!\")") } yield () } ``` :::note `ZLayer` is not only an effectful constructor, but also it supports concurrency and resource safety when constructing layers. ::: ## Step 5: Using ZIO Environment To Declare Dependencies So far, we learned that the `ZEnvironment` can act as an IoC container. Whenever we need a dependency, we can ask for it from the environment: ```scala val workflow: ZIO[Scope, Nothing, Unit] = for { env <- (ScalaFormatter.layer ++ ScalaCompiler.layer).build f <- env.get[Formatter].format("println(\"Hello, world!\")") _ <- env.get[Compiler].compile(f) } yield () ``` While this is a pretty good solution, there is a problem with it. Every time we need a dependency, we are asking for that instantly. In a large codebase, this imperative style of asking for dependencies can be tedious. This is an imperative style. It's better to make this declarative. So instead of **asking for dependencies** it is better to **declare dependencies**. Accordingly, we can use the `R` type-parameter of the `ZIO` data type which supports the declarative style: ```scala val workflow: ZIO[Compiler with Formatter, Nothing, String] = for { f <- ZIO.service[Formatter] r1 <- f.format("println(\"Hello, world!\")") c <- ZIO.service[Compiler] r1 <- c.compile(r1) } yield r1 ``` This is a much better solution. We just declare that we need the `Compiler` and the `Formatter` services using `ZIO.service` and then we compose pieces of our program to create the final application. The final workflow has all requirements in its type signature. For example, the `ZIO[Compiler with Formatter, Nothing, String]` type says that I need the `Compiler` and the `Formatter` services to produce the final result as a `String`. Finally, we can provide all the dependencies through the `ZIO#provideEnvironment` method: ```scala workflow.provideLayer(ScalaCompiler.layer ++ ScalaFormatter.layer) ``` ## Step 6: Automatic Dependency Graph Generation For large applications, it can be tedious to manually create the dependency graph. ZIO has a built-in mechanism empowered by using macros to automatically generate the dependency graph. To use this feature, we can use the `ZIO#provide` method: ```scala workflow.provide(ScalaCompiler.layer, ScalaFormatter.layer) ``` We should provide all required dependencies and then the ZIO will construct the dependency graph and provide that to our application. --- ## Overriding Dependency Graph We can create a ZIO application by providing a local or a global environment, or a combination: ## Global Environment It is usual when writing ZIO applications to provide layers at the end of the world. Then we provide layers to the whole ZIO application all at once. This pattern uses a single global environment for all ZIO applications: ```scala object MainApp extends ZIOAppDefault { val myApp: ZIO[ServiceA & ServiceB & ServiceC & ServiceD, Throwable, Unit] = ??? def run = myApp.provide(a, b, c, d) } ``` ## Local Environment Occasionally, we may need to provide different environments for different parts of our application, or it may be necessary to provide a single global environment for the entire application except for some inner layers. Providing a layer locally is analogous to overriding a method in an object-oriented paradigm. So we can think of that as overriding the global environment: ```scala object MainApp extends ZIOAppDefault { def myApp: ZIO[A & B & C, Throwable, Unit] = { def innerApp1: ZIO[A & B & C, Throwable, Unit] = ??? def innerApp2: ZIO[A & C, Throwable, Unit] = ??? innerApp1.provideSomeLayer[A & B](localC) *> innerApp2 } def run = myApp.provide(globalA, globalB, globalC) } ``` ZIO Test's [Live service](../test/services/live.md) uses this pattern to provide real environment to a single part of an effect. --- ## Providing Different Implementation of a Service One of the benefits of using dependency injection is that, we can write our application in a way that without modifying the application logic, we can provide different implementations of services to our application. ## Example 1: Config Service In the next example, we have a ZIO application that uses the `AppConfig` service: ```scala case class AppConfig(poolSize: Int) object AppConfig { def poolSize: ZIO[AppConfig, Nothing, Int] = ZIO.serviceWith[AppConfig](_.poolSize) val appArgsLayer: ZLayer[ZIOAppArgs, Nothing, AppConfig] = ZLayer { ZIOAppArgs.getArgs .map(_.headOption.map(_.toInt).getOrElse(8)) .map(poolSize => AppConfig(poolSize)) } val systemEnvLayer: ZLayer[Any, SecurityException, AppConfig] = ZLayer.fromZIO( System .env("POOL_SIZE") .map(_.headOption.map(_.toInt).getOrElse(8)) .map(poolSize => AppConfig(poolSize)) ) } object MainApp extends ZIOAppDefault { val myApp: ZIO[AppConfig, Nothing, Unit] = for { poolSize <- AppConfig.poolSize _ <- ZIO.debug(s"Application started with $poolSize pool size.") } yield () def run = myApp.provideSome[ZIOAppArgs](AppConfig.appArgsLayer) } ``` The `AppConfig` has two layers, `appArgsLayer` and `systemEnvLayer`. The first one uses command-line arguments to create the `AppConfig` and the second one uses environment variables. As we can see, without changing the core logic of our application, we can easily change the way we get the configuration: ```diff object MainApp extends ZIOAppDefault { val myApp: ZIO[AppConfig, Nothing, Unit] = for { poolSize <- AppConfig.poolSize _ <- ZIO.debug(s"Application started with $poolSize pool size.") } yield () - def run = myApp.provideSome[ZIOAppArgs](AppConfig.appArgsLayer) + def run = myApp.provide(AppConfig.systemEnvLayer) } ``` ## Example 2: Logging Service In this example, we have a ZIO application that uses the `Logging` service. And we provided two implementations of the `Logging` service: `SimpleLogger` and `DateTimeLogger`: ```scala trait Logging { def log(msg: String): ZIO[Any, IOException, Unit] } object Logging { def log(msg: String): ZIO[Logging, IOException, Unit] = ZIO.serviceWithZIO[Logging](_.log(msg)) } case class DateTimeLogger() extends Logging { override def log(msg: String): ZIO[Any, IOException, Unit] = for { dt <- Clock.currentDateTime _ <- Console.printLine(s"$dt: $msg") } yield () } object DateTimeLogger { val live: ULayer[DateTimeLogger] = ZLayer.succeed(DateTimeLogger()) } case class SimpleLogger() extends Logging { override def log(msg: String): ZIO[Any, IOException, Unit] = Console.printLine(msg) } object SimpleLogger { val live: ULayer[SimpleLogger] = ZLayer.succeed(SimpleLogger()) } ``` Now, let's write a ZIO application that uses the `Logging` service: ```scala val myApp: ZIO[Logging, IOException, Unit] = for { _ <- Logging.log("Application started.") _ <- Logging.log("Application ended.") } yield () ``` Now, we can run our application, just by providing one of the implementations of the `Logging` service. Let's run it with the `SimpleLogger` implementation: ```scala object MainApp extends ZIOAppDefault { def run = myApp.provide(SimpleLogger.live) } ``` Now, we can see that, without changing the core logic of our application, we can easily change the logger implementation: ```scala object MainApp extends ZIOAppDefault { def run = myApp.provide(DateTimeLogger.live) } ``` --- ## ZLayer: Constructor as a Value Before jumping into the next section, which will explain dependency injection in ZIO, let's take a look at the philosophy behind the `ZLayer` data type. In the [motivation](motivation.md) section, we find out that the ordinary Scala constructors are not powerful enough to help us to build the dependency graph easily. So `ZLayer` was created to overcome scala constructors' limitations. We can think of `ZLayer` as an alternative to constructors but with the following powerful features: - Composable with a nice ergonomic API - Asynchronous so it doesn't block the thread - Effectful and resourceful - Support for concurrency and parallelism Let's see the following example written using scala constructors: ```scala class Editor(formatter: Formatter, compiler: Compiler) { // ... } class Compiler() { // ... } class Formatter() { // ... } ``` We can say that each constructor is a function that takes some arguments as dependencies and returns a new instance of the class: - `() => Formatter` - `() => Compiler` - `(Formatter, Compiler) => Editor` The `ZLayer` reifies the conceptual idea of scala constructor and turned it into typed value which is equipped with lots of compositional operators and also supporting asynchronous operations. So in other words, `ZLayer` is a type-safe data type that describes the asynchronous, effectful and resourceful process of building the dependency graph. We can say that a `ZLayer[Input, E, Output]` is a recipe that takes some services as input and returns some services as output. For example, a `ZLayer` of type `ZLayer[Any, Nothing, Formatter]` is a constructor that doesn't take any services from the input and returns `Formatter` as output. Also, a `ZLayer` of type `ZLayer[Formatter with Compiler, Nothing, Editor]` is a constructor that takes `Formatter` and `Compiler` services from the input and returns `Editor` as output: ```scala object Formatter { val layer: ZLayer[Any, Nothing, Formatter] = ZLayer.succeed(new Formatter()) } object Compiler { val layer: ZLayer[Any, Nothing, Compiler] = ZLayer.succeed(new Compiler()) } object Editor { val layer: ZLayer[Formatter with Compiler, Nothing, Editor] = ZLayer { for { formatter <- ZIO.service[Formatter] compiler <- ZIO.service[Compiler] } yield new Editor(formatter, compiler) } } ``` ## Composable Constructors With scala constructors we compose services like the below to create the dependency graph: ```scala val formatter = new Formatter() val compiler = new Compiler() val editor = new Editor(formatter, compiler) ``` While Scala constructors are a type of Scala function. Composable functions in Scala are not as ergonomic as ZLayer for constructing dependency graphs. With ZLayer we can compose them using operators like `++` and `>>>`: ```scala val editor: ZLayer[Formatter with Compiler, Nothing, Editor] = (Formatter.layer ++ Compiler.layer) >>> Editor.layer ``` Also, we can compose `Formatter` and `Editor` layers to create a new layer that takes the `Compiler` service and returns the` Editor` service: ```scala val editor: ZLayer[Compiler, Nothing, Editor] = Formatter.layer >>> Editor.layer ``` ## Effectful Constructors If we have a dependency that requires an effectful computation to be initialized, we can't model easily such an operation using ordinary Scala constructors. In the following example, without the help of `ZIO#flatMap` or `ZLayer`, we can't easily create an instance of the `Editor` class: ```scala case class Counter(ref: Ref[Int]) { def inc: UIO[Unit] = ref.update(_ + 1) def dec: UIO[Unit] = ref.update(_ - 1) def get: UIO[Int] = ref.get } object Counter { // Effectful constructor def make: UIO[Counter] = Ref.make(0).map(new Counter(_)) } class Editor(formatter: Formatter, compiler: Compiler, counter: Counter) { // ... } object Formatter { def make = new Formatter() } object Compiler { def make = new Compiler() } val editor = new Editor( Formatter.make, Compiler.make, Counter.make // Compiler Error: Type mismatch: expected: Counter, found: UIO[Counter] ) ``` Let's see how we can use `ZIO#flatMap` to create `Editor`: ```scala val editor: ZIO[Any, Nothing, Editor] = Counter.make.map { counter => new Editor( Formatter.make, Compiler.make, counter ) } ``` While with `ZLayer`, we can easily have an effectful constructor. We can create `ZLayer` from any `ZIO` effect by using `ZLayer.fromZIO`/`ZLayer.apply` constructor: ```scala case class Counter(ref: Ref[Int]) { def inc: UIO[Unit] = ref.update(_ + 1) def dec: UIO[Unit] = ref.update(_ - 1) def get: UIO[Int] = ref.get } object Counter { val layer: ZLayer[Any, Nothing, Counter] = ZLayer { Ref.make(0).map(new Counter(_)) } } class Formatter { def format(code: String): UIO[String] = ??? } object Formatter { val layer: ZLayer[Any, Nothing, Formatter] = ZLayer.succeed(new Formatter()) } class Compiler { def compile(code: String): UIO[String] = ??? } object Compiler { val layer: ZLayer[Any, Nothing, Compiler] = ZLayer.succeed(new Compiler()) } class Editor(formatter: Formatter, compiler: Compiler, counter: Counter) { def formatAndCompile(code: String): UIO[String] = ??? } object Editor { val layer: ZLayer[Formatter with Compiler with Counter, Nothing, Editor] = ZLayer { for { formatter <- ZIO.service[Formatter] compiler <- ZIO.service[Compiler] counter <- ZIO.service[Counter] } yield new Editor(formatter, compiler, counter) } } ``` Let's try another example. Assume we have a `ZIO` effect that reads the application config from a file, we can create a layer from that: ```scala case class AppConfig(poolSize: Int) object AppConfig { private def loadConfig : Task[AppConfig] = ZIO.attempt(???) // loading config from a file val layer: TaskLayer[AppConfig] = ZLayer(loadConfig) // or ZLayer.fromZIO(loadConfig) } ``` ## Resourceful Constructors Some components of our applications need to be scoped, meaning they undergo a resource acquisition phase before usage, and a resource release phase after usage (e.g. when the application shuts down). As we stated before, the construction of ZIO layers can be effectful and resourceful, this means they can be acquired and safely released when the services are done being utilized. The `ZLayer` relies on the powerful `Scope` data type and this makes this process extremely simple. We can lift any scoped `ZIO` to `ZLayer` by providing a scoped resource to the `ZLayer.scoped` constructor: ```scala case class A(a: Int) object A { val layer: ZLayer[Any, Nothing, A] = ZLayer.scoped { ZIO.acquireRelease(acquire = ZIO.debug("Initializing A") *> ZIO.succeed(A(5)))( release = _ => ZIO.debug("Releasing A") ) } } object ZIOApp extends ZIOAppDefault { val myApp: ZIO[A, Nothing, Int] = for { a <- ZIO.serviceWith[A](_.a) } yield a * a def run = myApp .debug("result") .provide(A.layer) } ``` The output: ``` Initializing A result: 25 Releasing A ``` We can see that the `A` service is initialized and carefull released when the application is shut down. Here is another example that uses auto closeable resources: ```scala val fileLayer: ZLayer[Any, Throwable, BufferedSource] = ZLayer.scoped { ZIO.fromAutoCloseable( ZIO.attemptBlocking(scala.io.Source.fromFile("file.txt")) ) } ``` Finally, let's see a real-world example of creating a layer from scoped resources. Assume we have the following `UserRepository` service: ```scala trait DBConfig trait Transactor trait User def dbConfig: Task[DBConfig] = ZIO.attempt(???) def initializeDb(config: DBConfig): Task[Unit] = ZIO.attempt(???) def makeTransactor(config: DBConfig): ZIO[Scope, Throwable, Transactor] = ZIO.attempt(???) trait UserRepository { def save(user: User): Task[Unit] } case class UserRepositoryLive(xa: Transactor) extends UserRepository { override def save(user: User): Task[Unit] = ZIO.attempt(???) } ``` Assume we have written a scoped `UserRepository`: ```scala def scoped: ZIO[Scope, Throwable, UserRepository] = for { cfg <- dbConfig _ <- initializeDb(cfg) xa <- makeTransactor(cfg) } yield new UserRepositoryLive(xa) ``` We can convert that to `ZLayer` with `ZLayer.scoped`: ```scala object UserRepositoyLive { val layer : ZLayer[Any, Throwable, UserRepository] = ZLayer.scoped(scoped) } ``` ## Asynchronous Constructors We should avoid using blocking operations inside Scala constructors: ```scala class ProducerInput class KafkaProducer(input: ProducerInput) { def send(message: String): Unit = ??? } object KafkaProducer { def apply() = { // Blocking operation, we should avoid it inside constructors val input = doSomeBlockingOperation() new KafkaProducer(input) } private def doSomeBlockingOperation(): ProducerInput = ??? } ``` While with `ZLayer`, we can easily use blocking operations: ```scala class ProducerInput class KafkaProducer(input: ProducerInput) { def send(message: String): Task[Unit] = ??? } object KafkaProducer { val layer = ZLayer { for { input <- ZIO.attemptBlocking(doSomeBlockingOperation()) } yield (new KafkaProducer(input)) } private def doSomeBlockingOperation(): ProducerInput = ??? } ``` ## Parallel Constructors With `Zlayer` all layers in the dependency graph are executed in parallel: ```scala case class A(a: Int) object A { val layer: ZLayer[Any, Nothing, A] = ZLayer.fromZIO { for { _ <- ZIO.debug("Initializing A") _ <- ZIO.sleep(3.seconds) _ <- ZIO.debug("Initialized A") } yield A(1) } } case class B(b: Int) object B { val layer: ZLayer[Any, Nothing, B] = ZLayer.fromZIO { for { _ <- ZIO.debug("Initializing B") _ <- ZIO.sleep(2.seconds) _ <- ZIO.debug("Initialized B") } yield B(2) } } object ZIOApp extends ZIOAppDefault { val myApp: ZIO[A with B, Nothing, Int] = for { a <- ZIO.serviceWith[A](_.a) b <- ZIO.serviceWith[B](_.b) } yield a + b def run = myApp .debug("result") .provide(A.layer, B.layer) } ``` The output: ``` Initializing A Initializing B Initialized B Initialized A result: 3 ``` --- ## Model Domain Errors Using Algebraic Data Types It is best to use _algebraic data types (ADTs)_ when modeling errors within the same domain or subdomain. Sealed traits allow us to introduce an error type as a common supertype and all errors within a domain are part of that error type by extending that: ```scala sealed trait UserServiceError extends Exception case class InvalidUserId(id: ID) extends UserServiceError case class ExpiredAuth(id: ID) extends UserServiceError ``` In this case, the super error type is `UserServiceError`. We sealed that trait, and we extend it by two cases, `InvalidUserId` and `ExpiredAuth`. Because it is sealed, if we have a reference to a `UserServiceError` we can match against it and the Scala compiler knows there are two possibilities for a `UserServiceError`: ```scala userServiceError match { case InvalidUserId(id) => ??? case ExpiredAuth(id) => ??? } ``` This is a sum type, and also an enumeration. The Scala compiler knows only two of these `UserServiceError` exist. If we don't match on all of them, it is going to warn us. We can add the `-Xfatal-warnings` compiler option which treats warnings as errors. By turning on the fatal warning, we will have type-safety control on expected errors. So sealing these traits gives us great power. Also extending all of our errors from a common supertype helps the ZIO's combinators like flatMap to auto widen to the most specific error type. Let's say we have this for-comprehension here that calls the `userAuth` function, and it can fail with `ExpiredAuth`, and then we call `userProfile` that fails with `InvalidUserID`, and then we call `generateEmail` that can't fail at all, and finally we call `sendEmail` which can fail with `EmailDeliveryError`. We have got a lot of different errors here: ```scala val myApp: IO[Exception, Receipt] = for { service <- userAuth(token) // IO[ExpiredAuth, UserService] profile <- service.userProfile(userId) // IO[InvalidUserId, Profile] body <- generateEmail(orderDetails) // IO[Nothing, String] receipt <- sendEmail("Your order detail", body, profile.email) // IO[EmailDeliveryError, Unit] } yield receipt ``` In this example, the flatMap operations auto widens the error type to the most specific error type possible. As a result, the inferred error type of this for-comprehension will be `Exception` which gives us the best information we could hope to get out of this. We have lost information about the particulars of this. We no longer know which of these error types it is. We know it is some type of `Exception` which is more information than nothing. --- ## Don't Type Unexpected Errors When we first discover typed errors, it may be tempting to put every error into the error type parameter. That is a mistake because we can't recover from all types of errors. When we encounter unexpected errors we can't do anything in those cases. We should let the application die. Let it crash is the erlang philosophy. It is a good philosophy for all unexpected errors. At best, we can sandbox it, but we should let it crash. The context of a domain determines whether an error is expected or unexpected. When using typed errors, sometimes it is necessary to make a typed-error un-typed because in that case, we can't handle the error, and we should let the application crash. For example, in the following example, we don't want to handle the `IOException` so we can call `ZIO#orDie` to make the effect's failure unchecked. This will translate effect's failure to the death of the fiber running it: ```scala Console.printLine("Hello, World") // ZIO[Any, IOException, Unit] .orDie // ZIO[Any, Nothing, Unit] ``` If we have an effect that fails for some `Throwable` we can pick certain recoverable errors out of that, and then we can just let the rest of them kill the fiber that is running that effect. The ZIO effect has a method called `ZIO#refineOrDie` that allows us to do that. In the following example, calling `ZIO#refineOrDie` on an effect that has an error type `Throwable` allows us to refine it to have an error type of `TemporaryUnavailable`: ```scala val response: ZIO[Any, Nothing, Response] = ZIO .attemptBlocking( httpClient.fetchUrl(url) ) // ZIO[Any, Throwable, Response] .refineOrDie[TemporaryUnavailable] { case e: TemporaryUnavailable => e } // ZIO[Any, TemporaryUnavailable, Response] .retry( Schedule.fibonacci(1.second) ) // ZIO[Any, TemporaryUnavailable, Response] .orDie // ZIO[Any, Nothing, Response] ``` In this example, we are importing the `fetchUrl` which is a blocking operation into a `ZIO` value. We know that in case of a service outage it will throw the `TemporaryUnavailable` exception. This is an expected error, so we want that to be typed. We are going to reflect that in the error type. We only expect it, so we know how to recover from it. Also, this operation may throw unexpected errors like `OutOfMemoryError`, `StackOverflowError`, and so forth. Therefore, we don't include these errors since we won't be handling them at runtime. They are defects, and in case of unexpected errors, we should let the application crash. Therefore, it is quite common to import a code that may throw exceptions, whether that uses expected errors for error handling or can fail for a wide variety of unexpected errors like disk unavailable, service unavailable, and so on. Generally, importing these operations end up represented as a `Task` (`ZIO[Any, Throwable, A]`). So in order to make recoverable errors typed, we use the `ZIO#refineOrDie` method. --- ## Don't Reflexively Log Errors In modern async concurrent applications with a lot of subsystems, if we do not type errors, we are not able to see what section of our code fails with what error. Therefore, this can be very tempting to log errors when they happen. So when we lose type-safety in the whole application it makes us be more sensitive and program defensively. Therefore, whenever we are calling an API we tend to catch its errors, log them as below: ```scala sealed trait UploadError extends Exception case class FileExist(name: String) extends UploadError case class FileNotExist(name: String) extends UploadError case class StorageLimitExceeded(limit: Int) extends UploadError /** * This API fail with `FileExist` failure when the provided file name exist. */ def upload(name: String): Task[Unit] = { if (...) ZIO.fail(FileExist(name)) else if (...) ZIO.fail(StorageLimitExceeded(limit)) // This error is undocumented unintentionally else ZIO.attempt(...) } upload("contacts.csv").catchAll { case FileExist(name) => delete("contacts.csv") *> upload("contacts.csv") case _ => for { _ <- ZIO.log(error.toString) // logging the error _ <- ZIO.fail(error) // failing again (just like rethrowing exceptions in OOP) } yield () } ``` In the above code when we see the `upload`'s return type we can't find out what types of error it may fail with. So as a programmer we need to read the API documentation, and see in what cases it may fail. Due to the fact that the documents may be outdated, and they may not provide all error cases, we tend to add another case to cover all the other errors. Expert developers may prefer to read the implementation to find out all expected errors, but it is a tedious task to do. We don't want to lose any errors. So if we do not use typed errors, it makes us defensive to log every error, regardless of whether they will occur or not. When we are programming with typed errors, that allows us to never lose any errors. Even if we don't handle all, the error channel of our effect type demonstrate the type of remaining errors: ```scala val myApp: ZIO[Any, UploadError, Unit] = upload("contacts.csv") .catchSome { case FileExist(name) => delete(name) *> upload(name) } ``` It is still going to be sent an unhandled error type as a result. Therefore, there is no way to lose any errors, and they propagate automatically through all the different subsystems in our application, which means we don't have to be fearful anymore. It will be handled by higher-level code, or if it doesn't it will be passed off to something that can. If we handle all errors using `ZIO#catchAll` the type of error channel become `Nothing` which means there is no expected error remaining to handle: ```scala val myApp: ZIO[Any, Nothing, Unit] = upload("contacts.csv") .catchAll { case FileExist(name) => ZIO.unit // handling FileExist error case case StorageLimitExceeded(limit) => ZIO.unit // handling StorageLimitExceeded error case } ``` When we type errors, we know that they can't be lost. So typed errors give us the ability to log less. --- ## Use Union Types to Be More Specific About Error Types In Scala 3, we have an exciting new feature called union types. By using the union operator, we can encode multiple error types. Using this facility, we can have more precise information on typed errors. Let's see an example of `Storage` service which have `upload`, `download` and `delete` API: ```scala type Name = String enum StorageError extends Exception { case ObjectExist(name: Name) extends StorageError case ObjectNotExist(name: Name) extends StorageError case PermissionDenied(cause: String) extends StorageError case StorageLimitExceeded(limit: Int) extends StorageError case BandwidthLimitExceeded(limit: Int) extends StorageError } trait Storage { def upload( name: Name, obj: Array[Byte] ): ZIO[Any, ObjectExist | StorageLimitExceeded, Unit] def download( name: Name ): ZIO[Any, ObjectNotExist | BandwidthLimitExceeded, Array[Byte]] def delete(name: Name): ZIO[Any, ObjectNotExist | PermissionDenied, Unit] } ``` Union types allow us to get rid of the requirement to extend some sort of common error types like `Exception` or `Throwable`. This allows us to have completely unrelated error types. In the following example, the `FooError` and `BarError` are two distinct error. They have no super common type like `FooBarError` and also they are not extending `Exception` or `Throwable` classes: ```scala // Two unrelated errors without having a common supertype trait FooError trait BarError def foo: IO[FooError, Nothing] = ZIO.fail(new FooError {}) def bar: IO[BarError, Nothing] = ZIO.fail(new BarError {}) val myApp: ZIO[Any, FooError | BarError, Unit] = for { _ <- foo _ <- bar } yield () ``` --- ## Imperative vs. Declarative Error Handling To figure out the benefit of typed errors in declarative error handling, we need to understand the drawbacks of the imperative approach and then see how the declarative approach can be used to solve the same problem. ## Imperative Error Handling In the imperative style, when we encounter a wrong state, we throw an exception, and to handle exceptions, we have to use the `try`/`catch` language construct. Whenever we encounter an exception inside a `try` block, the control flow will jump to the `catch` block. In the catch block, we can handle the exception and decide what to do next. This is a very common pattern in imperative programming. For example, we can write the `divide` function like this: ```scala def divide(a: Int, b: Int): Int = if (b == 0) throw new IllegalArgumentException("Division by zero") else a / b ``` As we know that this function throws an exception when `b` is zero, so we need to handle the exception when we call this function: ```scala def readFromConsole: (Int, Int) = ??? val (a, b) = readFromConsole try { Some(divide(a, b)) } catch { case _: IllegalArgumentException => None } ``` ## Declarative Error Handling In declarative error handling, we treat errors as values instead of throwing exceptions. So instead of breaking the flow of the program, we can return a value that represents the error. When we have a workflow of type `ZIO[R, E, A]`, the `E` type parameter is used to represent that our workflow may fail with an error of type `E`. For example, the following program written with ZIO may fail with an error of type `AgeValidationException`: ```scala sealed trait AgeValidationException extends Exception case class NegativeAgeException(age: Int) extends AgeValidationException case class IllegalAgeException(age: Int) extends AgeValidationException def validate(age: Int): ZIO[Any, AgeValidationException, Int] = if (age < 0) ZIO.fail(NegativeAgeException(age)) else if (age < 18) ZIO.fail(IllegalAgeException(age)) else ZIO.succeed(age) ``` We can handle errors using `catchAll`/`catchSome` methods instead of using `try`/`catch` blocks: ```scala validate(17).catchAll { case NegativeAgeException(age) => ??? case IllegalAgeException(age) => ??? } ``` ## Imperative vs. Declarative ### Referential Transparency We say that an expression is referentially transparent if it can be replaced with its value without changing the behavior of the program. This property helps us to reason about a program easily. Also writing tests for our programs becomes much easier. Unfortunately, when we throw an exception, we lose the ability to reason about our programs. Exceptions break the referential transparency of our programs. For example, let's say we have the following function: ```scala def divide10By(b: Int): Option[Int] = { val result = divide(10, b) try { Some(result) } catch { case _: IllegalArgumentException => None } } ``` If we call `divide10By(0)`, we will get an exception (`IllegalArgumentException`). Now, let's see what happens if we replace the result with its value like this: ```scala def divide10By(b: Int): Option[Int] = try { Some(divide(10, b)) } catch { case _: IllegalArgumentException => None } ``` In this case, if we call `divide10By(0)`, we will get the `None` value. The behavior of the function will be changed. We cannot reason about the behavior of our program by substituting expressions with their values. In this style of error handling, the behavior of the program is dependent on where we call our expressions, inside or outside the `try` block. When we model our programs with the `ZIO`, we sure that our programs are referentially transparent. We can reason about our programs very easily without having to worry about changing the behavior of our programs. ### Type-safety There is no way to know what errors can be thrown by looking at the function signature. The only way to find out in which circumstance a method may throw an exception is to read and investigate its implementation. So the compiler cannot prevent us from writing unsafe codes. It is also hard for a developer to read the documentation event through reading the documentation is not sufficient as it may be obsolete, or it may don't reflect the exact exceptions. In ZIO when we see the type of the effect, we can determine what kind of error it can fail with. This helps us to have compile-time type-safety on our programs not only on success values but also on failure values. ### Exhaustivity Checking When we use `try`/`catch` the compiler doesn't know about errors at compile time, so if we forgot to handle one of the exceptions the compiler doesn't help us to write total functions. This code will crash at runtime because we forgot to handle the `IllegalAgeException` case: ```scala try { validate(17) } catch { case NegativeAgeException(age) => ??? // case IllegalAgeException(age) => ??? } ``` When we are using typed errors we can have exhaustive checking support from the compiler. For example, when we are catching all errors if we forgot to handle one of the cases, the compiler warns us about that: ```scala validate(17).catchAll { case NegativeAgeException(age) => ??? } // match may not be exhaustive. // It would fail on the following input: IllegalAgeException(_) ``` In the example above, if we only handle `NegativeAgeException`, the compiler will complain about the `IllegalAgeException` being unhandled. This helps us cover all cases and write _total functions_ easily. > **Note:** > > When a function is defined for all possible input values, it is called a _total function_ in functional programming. ### Error Model The error model based on the `try`/`catch`/`finally` statement is lossy and broken. Because if we have the combinations of these statements we can throw many exceptions, and then we are only able to catch one of them. All the other ones are lost. They are swallowed into a black hole, and also the one that we catch is the wrong one. It is not the primary cause of the failure. To be more specific, if the `try` block throws an exception, and the `finally` block throws an exception as well, then, if these are caught at a higher level, only the finalizer's exception will be caught normally, not the exception from the try block. In the following example, we are going to show this behavior: ```scala try { try throw new Error("e1") finally throw new Error("e2") } catch { case e: Error => println(e) } // Output: // e2 ``` The above program just prints the `e2` while it is not the primary cause of failure. That is why we say the `try`/`catch` model is lossy. In ZIO, all the errors will still be reported. So even though we are only able to catch one error, the other ones will be reported which we have full control over them. They don't get lost. Let's write a ZIO version: ```scala ZIO.fail("e1") .ensuring(ZIO.succeed(throw new Exception("e2"))) .catchAll { case "e1" => Console.printLine("e1") case "e2" => Console.printLine("e2") } // Output: // e1 ``` ZIO guarantees that no errors are lost. It has a _lossless error model_. This guarantee is provided via a hierarchy of supervisors and information made available via data types such as `Exit` and `Cause`. All errors will be reported. If there's a bug in the code, ZIO enables us to find out about it. --- ## Error Accumulation Sequential combinators such as `ZIO#zip` and `ZIO.foreach` stop when they reach the first error and return immediately. So their policy on error management is to fail fast. In the following example, we can see that the `ZIO#zip` operator will fail as soon as it reaches the first failure. As a result, we only see the first error in the stack trace: ```scala object MainApp extends ZIOAppDefault { val f1: ZIO[Any, Nothing, Int] = ZIO.succeed(1) val f2: ZIO[Any, String, Int] = ZIO.fail("Oh uh!").as(2) val f3: ZIO[Any, Nothing, Int] = ZIO.succeed(3) val f4: ZIO[Any, String, Int] = ZIO.fail("Oh no!").as(4) val myApp: ZIO[Any, String, (Int, Int, Int, Int)] = f1 zip f2 zip f3 zip f4 def run = myApp.debug } // Output: // Oh uh! // timestamp=2022-03-13T09:26:03.447149388Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.String: Oh uh! // at .MainApp.f2(MainApp.scala:5) // at .MainApp.myApp(MainApp.scala:10) // at .MainApp.run(MainApp.scala:12)" ``` There is also the `ZIO.foreach` operator that takes a collection and an effectful operation, then tries to apply the transformation to all elements of the collection. This operator also has the same error management behavior. It fails when it encounters the first error: ```scala object MainApp extends ZIOAppDefault { val myApp: ZIO[Any, String, List[Int]] = ZIO.foreach(List(1, 2, 3, 4, 5)) { n => if (n < 4) ZIO.succeed(n) else ZIO.fail(s"$n is not less that 4") } def run = myApp.debug } // Output: // 4 is not less that 4 // timestamp=2022-03-13T08:28:53.865690767Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.String: 4 is not less that 4 // at .MainApp.myApp(MainApp.scala:9) // at .MainApp.run(MainApp.scala:12)" ``` There are some situations when we need to collect all potential errors in a computation rather than failing fast. In this section, we will discuss operators that accumulate errors as well as successes. ## `ZIO#validate` It is similar to the `ZIO#zip` operator, it sequentially zips two ZIO effects together, if both effects fail, it combines their causes with `Cause.Then`: ```scala trait ZIO[-R, +E, +A] { def validate[R1 <: R, E1 >: E, B](that: => ZIO[R1, E1, B]): ZIO[R1, E1, (A, B)] } ``` If any of effecful operations doesn't fail, it results like the `zip` operator. Otherwise, when it reaches the first error it won't stop, instead, it will continue the zip operation until reach the final effect while combining: ```scala object MainApp extends ZIOAppDefault { val f1 = ZIO.succeed(1).debug val f2 = ZIO.succeed(2) *> ZIO.fail("Oh uh!") val f3 = ZIO.succeed(3).debug val f4 = ZIO.succeed(4) *> ZIO.fail("Oh error!") val f5 = ZIO.succeed(5).debug val myApp: ZIO[Any, String, (Int, Int, Int)] = f1 validate f2 validate f3 validate f4 validate f5 def run = myApp.cause.debug.uncause } // Output: // 1 // 3 // 5 // Then(Fail(Oh uh!,Trace(None,Chunk())),Fail(Oh error!,Trace(None,Chunk()))) // timestamp=2022-03-14T08:53:42.389942626Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.String: Oh uh! // at .MainApp.run(MainApp.scala:13) // Suppressed: java.lang.String: Oh error! // at .MainApp.run(MainApp.scala:13)" ``` The `ZIO#validatePar` operator is similar to the `ZIO#validate` operator zips two effects but in parallel. As this operator doesn't fail fast, unlike the `ZIO#zipPar` if it reaches a failure, it won't interrupt another running effect. If both effects fail, it will combine their causes with `Cause.Both`: ```scala object MainApp extends ZIOAppDefault { val f1 = ZIO.succeed(1).debug val f2 = ZIO.succeed(2) *> ZIO.fail("Oh uh!") val f3 = ZIO.succeed(3).debug val f4 = ZIO.succeed(4) *> ZIO.fail("Oh error!") val f5 = ZIO.succeed(5).debug val myApp: ZIO[Any, String, ((((Int, Int), Int), Int), Int)] = f1 validatePar f2 validatePar f3 validatePar f4 validatePar f5 def run = myApp.cause.map(_.untraced).debug.uncause } // One possible output: // 3 // 1 // 5 // Both(Fail(Oh uh!,Trace(None,Chunk())),Fail(Oh error!,Trace(None,Chunk()))) // timestamp=2022-03-14T09:16:00.670444190Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.String: Oh uh! // at .MainApp.run(MainApp.scala:13) // Exception in thread "zio-fiber-2" java.lang.String: Oh error! // at .MainApp.run(MainApp.scala:13)" ``` In addition, it has a `ZIO#validateWith` variant, which is useful for providing combiner function (`f: (A, B) => C`) to combine pair values. ## `ZIO.validate` This operator is very similar to the `ZIO.foreach` operator. It transforms all elements of a collection using the provided effectful operation, but it collects all errors in the error channel, as well as the success values in the success channel. It is similar to the `ZIO.partition` but it is an exceptional operator which means it collects errors in the error channel and success in the success channel: ```scala object ZIO { def validate[R, E, A, B](in: Collection[A])( f: A => ZIO[R, E, B] ): ZIO[R, ::[E], Collection[B]] def validate[R, E, A, B](in: NonEmptyChunk[A])( f: A => ZIO[R, E, B] ): ZIO[R, ::[E], NonEmptyChunk[B]] } ``` Another difference is that this operator is lossy, which means if there are errors all successes will be lost. In the lossy scenario, it will collect all errors in the error channel, which cause the failure: ```scala object MainApp extends ZIOAppDefault { val res: ZIO[Any, ::[String], List[Int]] = ZIO.validate(List.range(1, 7)){ n => if (n < 5) ZIO.succeed(n) else ZIO.fail(s"$n is not less that 5") } def run = res.debug } // Output: // List(5 is not less that 5, 6 is not less that 5) // timestamp=2022-03-12T07:34:36.510227783Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" scala.collection.immutable.$colon$colon: List(5 is not less that 5, 6 is not less that 5) // at .MainApp.res(MainApp.scala:5) // at .MainApp.run(MainApp.scala:11)" ``` In the success scenario when we have no errors at all, all the successes will be collected in the success channel: ```scala object MainApp extends ZIOAppDefault { val res: ZIO[Any, ::[String], List[Int]] = ZIO.validate(List.range(1, 4)){ n => if (n < 5) ZIO.succeed(n) else ZIO.fail(s"$n is not less that 5") } def run = res.debug } // Ouput: // List(1, 2, 3) ``` Two more notes: 1. The `ZIO.validate` operator is sequential, so we can use the `ZIO.validatePar` version to do the computation in parallel. 2. The `ZIO.validateDiscard` and `ZIO.validateParDiscard` operators are mostly similar to their non-discard versions, except they discard the successes. So the type of the success channel will be `Unit`. ## `ZIO.validateFirst` Like the `ZIO.validate` in the success scenario, it will collect all errors in the error channel except in the success scenario it will return only the first success: ```scala object ZIO { def validateFirst[R, E, A, B](in: Collection[A])( f: A => ZIO[R, E, B] ): ZIO[R, Collection[E], B] } ``` In the failure scenario, it will collect all errors in the failure channel, and it causes the failure: ```scala object MainApp extends ZIOAppDefault { val res: ZIO[Any, List[String], Int] = ZIO.validateFirst(List.range(5, 10)) { n => if (n < 5) ZIO.succeed(n) else ZIO.fail(s"$n is not less that 5") } def run = res.debug } // Output: // List(5 is not less that 5, 6 is not less that 5, 7 is not less that 5, 8 is not less that 5, 9 is not less that 5) // timestamp=2022-03-12T07:50:15.632883494Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" scala.collection.immutable.$colon$colon: List(5 is not less that 5, 6 is not less that 5, 7 is not less that 5, 8 is not less that 5, 9 is not less that 5) // at .MainApp.res(MainApp.scala:5) // at .MainApp.run(MainApp.scala:11)" ``` In the success scenario it will return the first success value: ```scala object MainApp extends ZIOAppDefault { val res: ZIO[Any, List[String], Int] = ZIO.validateFirst(List.range(1, 4)) { n => if (n < 5) ZIO.succeed(n) else ZIO.fail(s"$n is not less that 5") } def run = res.debug } // Output: // 1 ``` ## `ZIO.partition` The partition operator takes an iterable and effectful function that transforms each value of the iterable and finally creates a tuple of both failures and successes in the success channel. ```scala object ZIO { def partition[R, E, A, B](in: => Iterable[A])( f: A => ZIO[R, E, B] ): ZIO[R, Nothing, (Iterable[E], Iterable[B])] } ``` Note that this operator is an unexceptional effect, which means the type of the error channel is `Nothing`. So using this operator, if we reach a failure case, the whole effect doesn't fail. This is similar to the `List#partition` in the standard library: Let's try an example of collecting even numbers from the range of 0 to 7: ```scala val res: ZIO[Any, Nothing, (Iterable[String], Iterable[Int])] = ZIO.partition(List.range(0, 7)){ n => if (n % 2 == 0) ZIO.succeed(n) else ZIO.fail(s"$n is not even") } res.debug // Output: // (List(1 is not even, 3 is not even, 5 is not even),List(0, 2, 4, 6)) ``` --- ## Examples(Error-management) Let's write an application that takes numerator and denominator from the user and then print the result back to the user: ```scala object MainApp extends ZIOAppDefault { def run = for { a <- readNumber("Enter the first number (a): ") b <- readNumber("Enter the second number (b): ") r <- divide(a, b) _ <- Console.printLine(s"a / b: $r") } yield () def readNumber(msg: String): ZIO[Any, IOException, Int] = Console.print(msg) *> Console.readLine.map(_.toInt) def divide(a: Int, b: Int): ZIO[Any, Nothing, Int] = if (b == 0) ZIO.die(new ArithmeticException("divide by zero")) // unexpected error else ZIO.succeed(a / b) } ``` Now let's try to enter the zero for the second number and see what happens: ```scala Please enter the first number (a): 5 Please enter the second number (b): 0 timestamp=2022-02-14T09:39:53.981143209Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.ArithmeticException: divide by zero at MainApp$.$anonfun$divide$1(MainApp.scala:16) at zio.ZIO$.$anonfun$die$1(ZIO.scala:3384) at zio.internal.FiberContext.runUntil(FiberContext.scala:255) at zio.internal.FiberContext.run(FiberContext.scala:115) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630) at java.base/java.lang.Thread.run(Thread.java:831) at .MainApp.divide(MainApp.scala:16)" ``` As we see, because we entered the zero for the denominator, the `ArithmeticException` defect, makes the application crash. Defects are any _unexpected errors_ that we are not going to handle. They will propagate through our application stack until they crash the whole. Defects have many roots, most of them are from a programming error. Errors will happen when we haven't written the application with best practices. For example, one of these practices is that we should validate the inputs before providing them to the `divide` function. So if the user entered the zero as the denominator, we can retry and ask the user to return another number: ```scala object MainApp extends ZIOAppDefault { def run = for { a <- readNumber("Enter the first number (a): ") b <- readNumber("Enter the second number (b): ").repeatUntil(_ != 0) r <- divide(a, b) _ <- Console.printLine(s"a / b: $r") } yield () def readNumber(msg: String): ZIO[Any, IOException, Int] = Console.print(msg) *> Console.readLine.map(_.toInt) def divide(a: Int, b: Int): ZIO[Any, Nothing, Int] = ZIO.succeed(a / b) } ``` Another note about defects is that they are invisible, and they are not typed. We cannot expect what defects will happen by observing the typed error channel. In the above example, when we run the application and enter noninteger input, another defect, which is called `NumberFormatException` will crash the application: ```scala Enter the first number (a): five timestamp=2022-02-18T06:36:25.984665171Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.NumberFormatException: For input string: "five" at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67) at java.base/java.lang.Integer.parseInt(Integer.java:660) at java.base/java.lang.Integer.parseInt(Integer.java:778) at scala.collection.StringOps$.toInt$extension(StringOps.scala:910) at MainApp$.$anonfun$readNumber$3(MainApp.scala:16) at MainApp$.$anonfun$readNumber$3$adapted(MainApp.scala:16) ... at .MainApp.run(MainApp.scala:9)" ``` The cause of this defect is also a programming error, which means we haven't validated input when parsing it. So let's try to validate the input, and make sure that it is a number. We know that if the entered input does not contain a parsable `Int` the `String#toInt` throws the `NumberFormatException` exception. As we want this exception to be typed, we import the `String#toInt` function using the `ZIO.attempt` constructor. Using this constructor the function signature would be as follows: ```scala def parseInput(input: String): ZIO[Any, Throwable, Int] = ZIO.attempt(input.toInt) ``` Since the `NumberFormatException` is an expected error, and we want to handle it. So we type the error channel as `NumberFormatException`. To be more specific, we would like to narrow down the error channel to the `NumberFormatException`, so we can use the `refineToOrDie` operator: ```scala def parseInput(input: String): ZIO[Any, NumberFormatException, Int] = ZIO.attempt(input.toInt) // ZIO[Any, Throwable, Int] .refineToOrDie[NumberFormatException] // ZIO[Any, NumberFormatException, Int] ``` The same result can be achieved by succeeding the `String#toInt` and then widening the error channel using the `ZIO#unrefineTo` operator: ```scala def parseInput(input: String): ZIO[Any, NumberFormatException, Int] = ZIO.succeed(input.toInt) // ZIO[Any, Nothing, Int] .unrefineTo[NumberFormatException] // ZIO[Any, NumberFormatException, Int] ``` Now, let's refactor the example with recent changes: ```scala object MainApp extends ZIOAppDefault { def run = for { a <- readNumber("Enter the first number (a): ") b <- readNumber("Enter the second number (b): ").repeatUntil(_ != 0) r <- divide(a, b) _ <- Console.printLine(s"a / b: $r") } yield () def parseInput(input: String): ZIO[Any, NumberFormatException, Int] = ZIO.attempt(input.toInt).refineToOrDie[NumberFormatException] def readNumber(msg: String): ZIO[Any, IOException, Int] = (Console.print(msg) *> Console.readLine.flatMap(parseInput)) .retryUntil(!_.isInstanceOf[NumberFormatException]) .refineToOrDie[IOException] def divide(a: Int, b: Int): ZIO[Any, Nothing, Int] = ZIO.succeed(a / b) } ``` --- ## Exceptional and Unexceptional Effects Besides the `IO` type alias, ZIO has four different type aliases which can be categorized into two different categories: - **Exceptional Effect**— `Task` and `RIO` are two effects whose error parameter is fixed to `Throwable`, so we call them exceptional effects. - **Unexceptional Effect**— `UIO` and `URIO` have error parameters that are fixed to `Nothing`, indicating that they are unexceptional effects. So they can't fail, and the compiler knows about it. So when we compose different effects together, at any point of the codebase we can determine this piece of code can fail or cannot. As a result, typed errors offer a compile-time transition point between this can fail and this can't fail. For example, the `ZIO.acquireReleaseWith` API asks us to provide three different inputs: _acquire_, _release_, and _use_. The `release` parameter requires a function from `A` to `URIO[R, Any]`. So, if we put an exceptional effect, it will not compile: ```scala object ZIO { def acquireReleaseWith[R, E, A, B]( acquire: => ZIO[R, E, A], release: A => URIO[R, Any], use: A => ZIO[R, E, B] ): ZIO[R, E, B] } ``` --- ## Expected and Unexpected Errors Inside an application, there are two distinct categories of errors: - **Expected errors** are those that are expected to occur, and we tend to recover them. They are also known as _recoverable errors_ or _declared errors_. - **Unexpected errors** are those that are not expected to occur, and they are not recoverable. They are also known as _non-recoverable errors_ or _defects_. ## Expected Errors Expected errors are those errors in which we expected them to happen in normal circumstances, and we can't prevent them. They can be predicted upfront, and we can plan for them. We know when, where, and why they occur. So we know when, where, and how to handle these errors. By handling them we can recover from the failure, this is why we say they are _recoverable errors_. All domain errors, business errors are expected once because we talk about them in workflows and user stories, so we know about them in the context of business flows. For example, when accessing an external database, that database might be down for some short period of time, so we retry to connect again, or after some number of attempts, we might decide to use an alternative solution, e.g. using an in-memory database. ## Unexpected Errors We know there is a category of things that we are not going to expect and plan for. These are the things we don't expect but of course, we know they are going to happen. We don't know what is the exact root of these errors at runtime, so we have no idea how to handle them. They are actually going to bring down our production application, and then we have to figure out what went wrong to fix them. For example, the corrupted database file will cause an unexpected error. We can't handle that in runtime. It may be necessary to shut down the whole application in order to prevent further damage. Most of the unexpected errors are rooted in programming errors. This means, we have just tested the _happy path_, so in case of _unhappy path_ we encounter a defect. When we have defects in our code we have no way of knowing about them otherwise we investigate, test, and fix them. One of the common programming errors is forgetting to validate unexpected errors that may occur when we expect an input but the input is not valid, while we haven't validated the input. When the user inputs the invalid data, we might encounter the divide by zero exception or might corrupt our service state or a cause similar defect. These kinds of defects are common when we upgrade our service with the new data model for its input, while one of the other services is not upgraded with the new data contract and is calling our service with the deprecated data model. If we haven't a validation phase, they will cause defects! Another example of defects is _memory errors_ like _buffer overflows_, _stack overflows_, _out-of-memory_, _invalid access to null pointers_, and so forth. Most of the time these unexpected errors are occurs when we haven't written a memory-safe and resource-safe program, or they might occur due to hardware issues or uncontrollable external problems. We as a developer don't know how to cope with these types of errors at runtime. We should investigate to find the exact root cause of these defects. As we cannot handle unexpected errors, we should instead log them with their respective stack traces and contextual information. So later we could investigate the problem and try to fix them. The best we can do with unexpected errors is to _sandbox_ them to limit the damage that they do to the overall application. For example, an unexpected error in browser extension shouldn't crash the whole browser. ## Best Practices So the best practice for each of these errors is as follows: ### Expected Errors we handle expected errors with the aid of the Scala compiler, by pushing them into the type system. In ZIO there is the error type parameter called `E`, and this error type parameter is for modeling all the expected errors in the application. A ZIO value has a type parameter `E` which is the type of _declared errors_ it can fail with. `E` only covers the errors which were specified at the outset. The same ZIO value could still throw exceptions in unforeseen ways. These unforeseen situations are called _defects_ in a ZIO program, and they lie outside `E`. Bringing abnormal situations from the domain of defects into that of `E` enables the compiler to help us keep a tab on error conditions throughout the application, at compile time. This helps ensure the handling of domain errors in domain-specific ways. ### Unexpected Errors We encode unexpected errors by not reflecting them to the type system because there is no way we could do it, and it wouldn't provide any value if we could. At best as we can, we simply sandbox that to some well-defined area of the application. Note that _defects_, can creep silently to higher levels in our application, and, if they get triggered at all, their handling might eventually be in more general ways. So for ZIO, expected errors are reflected in the type of the ZIO effect, whereas unexpected errors are not so reflective, and that is the distinction. That is the best practice. It helps us write better code. The code that we can reason about its error properties and potential expected errors. We can look at the ZIO effect and know how it is supposed to fail. ## Conclusion 1. Unexpected errors are impossible to recover, and they will eventually shut down the application but expected errors can be recovered by handling them. 2. We do not type unexpected errors, but we type expected errors either explicitly or using general `Throwable` error type. 3. Unexpected errors mostly is a sign of programming errors, but expected errors part of domain errors. 4. Even though we haven't any clue on how to handle defects, we might still need to do some operation, before letting them crash the application. So in such a situation, we can [catch defects](recovering/catching.md) do following operations, and then rethrow them again: - logging the defect to a log aggregator - sending an email to alert developers - displaying a nice "unexpected error" message to the user - etc. --- ## Introduction to Error Management in ZIO As well as providing first-class support for typed errors, ZIO has a variety of facilities for catching, propagating, and transforming errors in a typesafe manner. In this section, we will learn about different types of errors in ZIO and how we can manage them. --- ## Chaining Effects Based on Errors Unlike `ZIO#flatMap` the `ZIO#flatMapError` combinator chains two effects, where the second effect is dependent on the error channel of the first effect: ```scala trait ZIO[-R, +E, +A] { def flatMapError[R1 <: R, E2]( f: E => ZIO[R1, Nothing, E2] ): ZIO[R1, E2, A] } ``` In the following example, we are trying to find a random prime number between 1000 and 10000. We will use the `ZIO#flatMapError` to collect all errors inside a `Ref` of type `List[String]`: ```scala object MainApp extends ZIOAppDefault { def isPrime(n: Int): Boolean = if (n <= 1) false else (2 until n).forall(i => n % i != 0) def findPrimeBetween( minInclusive: Int, maxExclusive: Int ): ZIO[Any, List[String], Int] = for { errors <- Ref.make(List.empty[String]) number <- Random .nextIntBetween(minInclusive, maxExclusive) .reject { case n if !isPrime(n) => s"non-prime number rejected: $n" } .flatMapError(error => errors.updateAndGet(_ :+ error)) .retryUntil(_.length >= 5) } yield number val myApp: ZIO[Any, Nothing, Unit] = findPrimeBetween(1000, 10000) .flatMap(prime => Console.printLine(s"found a prime number: $prime").orDie) .catchAll { (errors: List[String]) => Console.printLine( s"failed to find a prime number after 5 attempts:\n ${errors.mkString("\n ")}" ) } .orDie def run = myApp } ``` --- ## Converting Defects to Failures Both `ZIO#resurrect` and `ZIO#absorb` are symmetrical opposite of the `ZIO#orDie` operator. The `ZIO#orDie` takes failures from the error channel and converts them into defects, whereas the `ZIO#absorb` and `ZIO#resurrect` take defects and convert them into failures: ```scala trait ZIO[-R, +E, +A] { def absorb(implicit ev: E IsSubtypeOfError Throwable): ZIO[R, Throwable, A] def absorbWith(f: E => Throwable): ZIO[R, Throwable, A] def resurrect(implicit ev1: E IsSubtypeOfError Throwable): ZIO[R, Throwable, A] } ``` Below are examples of the `ZIO#absorb` and `ZIO#resurrect` operators: ```scala val effect1 = ZIO.fail(new IllegalArgumentException("wrong argument")) // ZIO[Any, IllegalArgumentException, Nothing] .orDie // ZIO[Any, Nothing, Nothing] .absorb // ZIO[Any, Throwable, Nothing] .refineToOrDie[IllegalArgumentException] // ZIO[Any, IllegalArgumentException, Nothing] val effect2 = ZIO.fail(new IllegalArgumentException("wrong argument")) // ZIO[Any, IllegalArgumentException , Nothing] .orDie // ZIO[Any, Nothing, Nothing] .resurrect // ZIO[Any, Throwable, Nothing] .refineToOrDie[IllegalArgumentException] // ZIO[Any, IllegalArgumentException, Nothing] ``` So what is the difference between `ZIO#absorb` and `ZIO#resurrect` operators? The `ZIO#absorb` can recover from both `Die` and `Interruption` causes. Using this operator we can absorb failures, defects and interruptions using `ZIO#absorb` operation. It attempts to convert all causes into a failure, throwing away all information about the cause of the error: ```scala object MainApp extends ZIOAppDefault { val effect1 = ZIO.dieMessage("Boom!") // ZIO[Any, Nothing, Nothing] .absorb // ZIO[Any, Throwable, Nothing] .ignore val effect2 = ZIO.interrupt // ZIO[Any, Nothing, Nothing] .absorb // ZIO[Any, Throwable, Nothing] .ignore def run = (effect1 <*> effect2) .debug("application exited successfully") } ``` The output would be as below: ```scala application exited successfully: () ``` Whereas, the `ZIO#resurrect` will only recover from `Die` causes: ```scala object MainApp extends ZIOAppDefault { val effect1 = ZIO .dieMessage("Boom!") // ZIO[Any, Nothing, Nothing] .resurrect // ZIO[Any, Throwable, Nothing] .ignore val effect2 = ZIO.interrupt // ZIO[Any, Nothing, Nothing] .resurrect // ZIO[Any, Throwable, Nothing] .ignore def run = (effect1 <*> effect2) .debug("couldn't recover from fiber interruption") } ``` And, here is the output: ```scala timestamp=2022-02-18T14:21:52.559872464Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.InterruptedException: Interrupted by thread "zio-fiber-" at .MainApp.effect2(MainApp.scala:10) at .MainApp.effect2(MainApp.scala:11) at .MainApp.effect2(MainApp.scala:12) at .MainApp.run(MainApp.scala:15) at .MainApp.run(MainApp.scala:16)" ``` --- ## Error Refinement ZIO has some operators useful for converting defects into failures. So we can take part in non-recoverable errors and convert them into the typed error channel and vice versa. Note that both `ZIO#refine*` and `ZIO#unrefine*` do not alter the error behavior, but only change the error model. That is to say, if an effect fails or die, then after `ZIO#refine*` or `ZIO#unrefine*`, it will still fail or die; and if an effect succeeds, then after `ZIO#refine*` or `ZIO#unrefine*`, it will still succeed; only the manner in which it signals the error will be altered by these two methods: 1. The `ZIO#refine*` pinches off a piece of _failure_ of type `E`, and converts it into a _defect_. 2. The `ZIO#unrefine*` pinches off a piece of a _defect_, and converts it into a _failure_ of type `E`. ## Refining ### `ZIO#refineToOrDie` This operator **narrows** down the type of the error channel from `E` to the `E1`. It leaves the rest errors untyped, so everything that doesn't fit is turned into a defect. So it makes the error space **smaller**. ```scala ZIO[-R, +E, +A] { def refineToOrDie[E1 <: E]: ZIO[R, E1, A] } ``` In the following example, we are going to implement `parseInt` by importing `String#toInt` code from the standard scala library using `ZIO#attempt` and then refining the error channel from `Throwable` to the `NumberFormatException` error type: ```scala def parseInt(input: String): ZIO[Any, NumberFormatException, Int] = ZIO.attempt(input.toInt) // ZIO[Any, Throwable, Int] .refineToOrDie[NumberFormatException] // ZIO[Any, NumberFormatException, Int] ``` In this example, if the `input.toInt` throws any other exceptions other than `NumberFormatException`, e.g. `IndexOutOfBoundsException`, will be translated to the ZIO defect. ### `ZIO#refineOrDie` It is the more powerful version of the previous operator. Instead of refining to one specific error type, we can refine to multiple error types using a partial function: ```scala trait ZIO[-R, +E, +A] { def refineOrDie[E1](pf: PartialFunction[E, E1]): ZIO[R, E1, A] } ``` In the following example, we excluded the `Baz` exception from recoverable errors, so it will be converted to a defect. In another word, we narrowed `DomainError` down to just `Foo` and `Bar` errors: ```scala sealed abstract class DomainError(msg: String) extends Exception(msg) with Serializable with Product case class Foo(msg: String) extends DomainError(msg) case class Bar(msg: String) extends DomainError(msg) case class Baz(msg: String) extends DomainError(msg) object MainApp extends ZIOAppDefault { val effect: ZIO[Any, DomainError, Unit] = ZIO.fail(Baz("Oh uh!")) val refined: ZIO[Any, DomainError, Unit] = effect.refineOrDie { case foo: Foo => foo case bar: Bar => bar } def run = refined.catchAll(_ => ZIO.unit).debug } ``` ### `ZIO#refineOrDieWith` In the two previous refine combinators, we were dealing with exceptional effects whose error channel type was `Throwable` or a subtype of that. The `ZIO#refineOrDieWith` operator is a more powerful version of refining operators. It can work with any exceptional effect whether they are `Throwable` or not. When we narrow down the failure space, some failures become defects. To convert those failures to defects, it takes a function from `E` to `Throwable`: ```scala trait ZIO[-R, +E, +A] { def refineOrDieWith[E1](pf: PartialFunction[E, E1])(f: E => Throwable): ZIO[R, E1, A] } ``` In the following example, we excluded the `BazError` from recoverable errors, so it will be converted to a defect. In another word, we narrowed the whole space of `String` errors down to just "FooError" and "BarError": ```scala object MainApp extends ZIOAppDefault { def effect(i: String): ZIO[Any, String, Nothing] = { if (i == "foo") ZIO.fail("FooError") else if (i == "bar") ZIO.fail("BarError") else ZIO.fail("BazError") } val refined: ZIO[Any, String, Nothing] = effect("baz").refineOrDieWith { case "FooError" | "BarError" => "Oh Uh!" }(e => new Throwable(e)) def run = refined.catchAll(_ => ZIO.unit) } ``` ## Unrefining ### `ZIO#unrefineTo[E1 >: E]` This operator **broadens** the type of the error channel from `E` to the `E1` and embeds some defects into it. So it is going from some fiber failures back to errors and thus making the error type **larger**: ```scala trait ZIO[-R, +E, +A] { def unrefineTo[E1 >: E]: ZIO[R, E1, A] } ``` In the following example, we are going to implement `parseInt` by importing `String#toInt` code from the standard scala library using `ZIO#succeed` and then unrefining the error channel from `Nothing` to the `NumberFormatException` error type: ```scala def parseInt(input: String): ZIO[Any, NumberFormatException, Int] = ZIO.succeed(input.toInt) // ZIO[Any, Nothing, Int] .unrefineTo[NumberFormatException] // ZIO[Any, NumberFormatException, Int] ``` ### `ZIO#unrefine` It is a more powerful version of the previous operator. It takes a partial function from `Throwable` to `E1` and converts those defects to recoverable errors: ```scala trait ZIO[-R, +E, +A] { def unrefine[E1 >: E](pf: PartialFunction[Throwable, E1]): ZIO[R, E1, A] } ``` ```scala case class Foo(msg: String) extends Throwable(msg) case class Bar(msg: String) extends Throwable(msg) case class Baz(msg: String) extends Throwable(msg) object MainApp extends ZIOAppDefault { def unsafeOpThatMayThrows(i: String): String = if (i == "foo") throw Foo("Oh uh!") else if (i == "bar") throw Bar("Oh Error!") else if (i == "baz") throw Baz("Oh no!") else i def effect(i: String): ZIO[Any, Nothing, String] = ZIO.succeed(unsafeOpThatMayThrows(i)) val unrefined: ZIO[Any, Foo, String] = effect("foo").unrefine { case e: Foo => e } def run = unrefined.catchAll(_ => ZIO.unit) } ``` Using `ZIO#unrefine` we can have more control to unrefine a ZIO effect that may die because of some defects, for example in the following example we are going to convert both `Foo` and `Bar` defects to recoverable errors and remain `Baz` unrecoverable: ```scala val unrefined: ZIO[Any, Throwable, String] = effect("foo").unrefine { case e: Foo => e case e: Bar => e } ``` ### `ZIO#unrefineWith` This is the most powerful version of unrefine operators. It takes a partial function, as the previous operator, and then tries to broaden the failure space by converting some of the defects to typed recoverable errors. If it doesn't find any defect, it will apply the `f` which is a function from `E` to `E1`, and map all typed errors using this function: ```scala trait ZIO[-R, +E, +A] { def unrefineWith[E1](pf: PartialFunction[Throwable, E1])(f: E => E1): ZIO[R, E1, A] } ``` ```scala object MainApp extends ZIOAppDefault { case class Foo(msg: String) extends Exception(msg) case class Bar(msg: String) extends Exception(msg) val effect: ZIO[Any, Foo, Nothing] = ZIO.ifZIO(Random.nextBoolean)( onTrue = ZIO.fail(Foo("Oh uh!")), onFalse = ZIO.die(Bar("Boom!")) ) val unrefined: ZIO[Any, String, Nothing] = effect .unrefineWith { case e: Bar => e.getMessage }(e => e.getMessage) def run = unrefined.cause.debug } ``` --- ## Exposing Errors in The Success Channel Before taking into `ZIO#either` and `ZIO#absolve`, let's see their signature: ```scala trait ZIO[-R, +E, +A] { def either(implicit ev: CanFail[E]): URIO[R, Either[E, A]] def absolve[E1 >: E, B](implicit ev: A IsSubtypeOfOutput Either[E1, B]): ZIO[R, E1, B] } ``` Before continuing, let's take a look again at the `validate` function we have written earlier: ```scala sealed trait AgeValidationException extends Exception case class NegativeAgeException(age: Int) extends AgeValidationException case class IllegalAgeException(age: Int) extends AgeValidationException def validate(age: Int): ZIO[Any, AgeValidationException, Int] = if (age < 0) ZIO.fail(NegativeAgeException(age)) else if (age < 18) ZIO.fail(IllegalAgeException(age)) else ZIO.succeed(age) ``` Now we are ready to use `ZIO#either` and `ZIO#absolve` operations: ## `ZIO#either` The `ZIO#either` convert a `ZIO[R, E, A]` effect to another effect in which its failure (`E`) and success (`A`) channel have been lifted into an `Either[E, A]` data type as success channel of the `ZIO` data type: ```scala val age: Int = ??? val res: URIO[Any, Either[AgeValidationException, Int]] = validate(age).either ``` The resulting effect is an unexceptional effect and cannot fail, because the failure case has been exposed as part of the `Either` success case. The error parameter of the returned `ZIO` is `Nothing`, since it is guaranteed the `ZIO` effect does not model failure. This method is useful for recovering from `ZIO` effects that may fail: ```scala val myApp: ZIO[Any, IOException, Unit] = for { _ <- Console.print("Please enter your age: ") age <- Console.readLine.map(_.toInt) res <- validate(age).either _ <- res match { case Left(error) => ZIO.debug(s"validation failed: $error") case Right(age) => ZIO.debug(s"The $age validated!") } } yield () ``` ## `ZIO#absolve`/`ZIO.absolve` The `ZIO#abolve` operator and the `ZIO.absolve` constructor perform the inverse. They submerge the error case of an `Either` into the `ZIO`: ```scala val age: Int = ??? validate(age) // ZIO[Any, AgeValidationException, Int] .either // ZIO[Any, Either[AgeValidationException, Int]] .absolve // ZIO[Any, AgeValidationException, Int] ``` Here is another example: ```scala def sqrt(input: ZIO[Any, Nothing, Double]): ZIO[Any, String, Double] = ZIO.absolve( input.map { value => if (value < 0.0) Left("Value must be >= 0.0") else Right(Math.sqrt(value)) } ) ``` --- ## Exposing the Cause in The Success Channel Using the `ZIO#cause` operation we can expose the cause, and then by using `ZIO#uncause` we can reverse this operation: ```scala trait ZIO[-R, +E, +A] { def cause: URIO[R, Cause[E]] def uncause[E1 >: E](implicit ev: A IsSubtypeOfOutput Cause[E1]): ZIO[R, E1, Unit] } ``` In the following example, we expose and then untrace the underlying cause: ```scala object MainApp extends ZIOAppDefault { val f1: ZIO[Any, String, Int] = ZIO.fail("Oh uh!").as(1) val f2: ZIO[Any, String, Int] = ZIO.fail("Oh error!").as(2) val myApp: ZIO[Any, String, (Int, Int)] = f1 zipPar f2 def run = myApp.cause.map(_.untraced).debug } ``` Sometimes the [`ZIO#mapErrorCause`](map-operations.md#ziomaperrorziomaperrorcause) operator is a better choice when we just want to map the underlying cause without exposing the cause. --- ## Filtering the Success Channel ZIO has a variety of operators that can filter values on the success channel based on a given predicate, and if the predicate fails, we can use different strategies: - Failing the original effect (`ZIO#filterOrFail`) - Dying the original effect (`ZIO#filterOrDie` and `ZIO#filterOrDieMessage`) - Running an alternative ZIO effect (`ZIO#filterOrElse` and `ZIO#filterOrElseWith`) ```scala def getNumber: ZIO[Any, Nothing, Int] = (Console.print("Please enter a non-negative number: ") *> Console.readLine.mapAttempt(_.toInt)) .retryUntil(!_.isInstanceOf[NumberFormatException]).orDie val r1: ZIO[Any, String, Int] = Random.nextInt.filterOrFail(_ >= 0)("random number is negative") val r2: ZIO[Any, Nothing, Int] = Random.nextInt.filterOrDie(_ >= 0)( new IllegalArgumentException("random number is negative") ) val r3: ZIO[Any, Nothing, Int] = Random.nextInt.filterOrDieMessage(_ >= 0)("random number is negative") val r4: ZIO[Any, Nothing, Int] = Random.nextInt.filterOrElse(_ >= 0)(getNumber) val r5: ZIO[Any, Nothing, Int] = Random.nextInt.filterOrElseWith(_ >= 0)(x => ZIO.succeed(-x)) ``` --- ## Flattening Optional Error Types If we have an optional error of type `E` in the error channel, we can flatten it to the `E` type using the `ZIO#flattenErrorOption` operator: ```scala def parseInt(input: String): ZIO[Any, Option[String], Int] = if (input.isEmpty) ZIO.fail(Some("empty input")) else try { ZIO.succeed(input.toInt) } catch { case _: NumberFormatException => ZIO.fail(None) } def flattenedParseInt(input: String): ZIO[Any, String, Int] = parseInt(input).flattenErrorOption("non-numeric input") val r1: ZIO[Any, String, Int] = flattenedParseInt("zero") val r2: ZIO[Any, String, Int] = flattenedParseInt("") val r3: ZIO[Any, String, Int] = flattenedParseInt("123") ``` --- ## Flipping Error and Success Channels Sometimes, we would like to apply some methods on the error channel which are specific for the success channel, or we want to apply some methods on the success channel which are specific for the error channel. Therefore, we can flip the error and success channel and before flipping back, we can perform the right operator on flipped channels: ```scala trait ZIO[-R, +E, +A] { def flip: ZIO[R, A, E] def flipWith[R1, A1, E1](f: ZIO[R, A, E] => ZIO[R1, A1, E1]): ZIO[R1, E1, A1] } ``` Assume we have the following example: ```scala val evens: ZIO[Any, List[String], List[Int]] = ZIO.validate(List(1, 2, 3, 4, 5)) { n => if (n % 2 == 0) ZIO.succeed(n) else ZIO.fail(s"$n is not even") } ``` We want to reverse the order of errors. In order to do that instead of using `ZIO#mapError`, we can map the error channel by using flip operators: ```scala val r1: ZIO[Any, List[String], List[Int]] = evens.mapError(_.reverse) val r2: ZIO[Any, List[String], List[Int]] = evens.flip.map(_.reverse).flip val r3: ZIO[Any, List[String], List[Int]] = evens.flipWith(_.map(_.reverse)) ``` --- ## Map Operations Other than `ZIO#map` and `ZIO#flatMap`, ZIO has several other operators to manage errors while mapping: ## `ZIO#mapError`/`ZIO#mapErrorCause` Let's begin with `ZIO#mapError` and `ZIO#mapErrorCause`. These operators help us to access the error channel as a raw error value or as a type of `Cause` and map their values: ```scala trait ZIO[-R, +E, +A] { def mapError[E2](f: E => E2): ZIO[R, E2, A] def mapErrorCause[E2](h: Cause[E] => Cause[E2]): ZIO[R, E2, A] } ``` Here are two simple examples for these operators: ```scala def parseInt(input: String): ZIO[Any, NumberFormatException, Int] = ??? // mapping the error of the original effect to its message val r1: ZIO[Any, String, Int] = parseInt("five") // ZIO[Any, NumberFormatException, Int] .mapError(e => e.getMessage) // ZIO[Any, String, Int] // mapping the cause of the original effect to be untraced val r2 = parseInt("five") // ZIO[Any, NumberFormatException, Int] .mapErrorCause(_.untraced) // ZIO[Any, NumberFormatException, Int] ``` > _**Note:**_ > > Note that mapping over an effect's success or error channel does not change the success or failure of the effect, in the same way that mapping over an `Either` does not change whether the `Either` is `Left` or `Right`. ## `ZIO#mapAttempt` The `ZIO#mapAttempt` returns an effect whose success is mapped by the specified side-effecting `f` function, translating any thrown exceptions into typed failed effects. So it converts an unchecked exception to a checked one by returning the `RIO` effect. ```scala trait ZIO[-R, +E, +A] { def map[B](f: A => B): ZIO[R, E, B] def mapAttempt[B](f: A => B): ZIO[R, Throwable, B] } ``` Using operations that can throw exceptions inside of `ZIO#map` such as `effect.map(_.unsafeOpThatThrows)` will result in a defect (an unexceptional effect that will die). In the following example, when we use the `ZIO#map` operation. So, if the `String#toInt` operation throws `NumberFormatException` it will be converted to a defect: ```scala val result: ZIO[Any, Nothing, Int] = Console.readLine.orDie.map(_.toInt) ``` As a result, when the map operation is unsafe, it may lead to buggy programs that may crash, as shown below: ```scala object MainApp extends ZIOAppDefault { val myApp: ZIO[Any, Nothing, Unit] = Console.print("Please enter a number: ").orDie *> Console.readLine.orDie .map(_.toInt) .map(_ % 2 == 0) .flatMap { case true => Console.printLine("You have entered an even number.").orDie case false => Console.printLine("You have entered an odd number.").orDie } def run = myApp } ``` Converting literal "five" String to Int by calling `toInt` is a side effecting operation because it will throw `NumberFormatException` exception. So in the previous example, if we enter a non-integer number, e.g. "five", it will die because of a `NumberFormatException` defect: ```scala Please enter a number: five timestamp=2022-03-17T14:01:33.323639073Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.NumberFormatException: For input string: "five" at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67) at java.base/java.lang.Integer.parseInt(Integer.java:660) at java.base/java.lang.Integer.parseInt(Integer.java:778) at scala.collection.StringOps$.toInt$extension(StringOps.scala:910) at MainApp$.$anonfun$myApp$3(MainApp.scala:7) at MainApp$.$anonfun$myApp$3$adapted(MainApp.scala:7) at zio.ZIO.$anonfun$map$1(ZIO.scala:1168) at zio.ZIO$FlatMap.apply(ZIO.scala:6182) at zio.ZIO$FlatMap.apply(ZIO.scala:6171) at zio.internal.FiberContext.runUntil(FiberContext.scala:885) at zio.internal.FiberContext.run(FiberContext.scala:115) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630) at java.base/java.lang.Thread.run(Thread.java:831) at zio.internal.FiberContext.runUntil(FiberContext.scala:538) at .MainApp.myApp(MainApp.scala:8) at .MainApp.myApp(MainApp.scala:9)" ``` We can see that the error channel of `myApp` is typed as `Nothing`, so it's not an exceptional error. If we want typed effects, this behavior is not intended. So instead of `ZIO#map` we can use the `mapAttempt` combinator which is a safe map operator that translates all thrown exceptions into typed exceptional effect. To prevent converting exceptions to defects, we can use `ZIO#mapAttempt` which converts any exceptions to exceptional effects: ```scala val result: ZIO[Any, Throwable, Int] = Console.readLine.orDie.mapAttempt(_.toInt) ``` Having typed errors helps us to catch errors explicitly and handle them in the right way: ```scala object MainApp extends ZIOAppDefault { val myApp: ZIO[Any, Nothing, Unit] = Console.print("Please enter a number: ").orDie *> Console.readLine.orDie .mapAttempt(_.toInt) .map(_ % 2 == 0) .flatMap { case true => Console.printLine("You have entered an even number.").orDie case false => Console.printLine("You have entered an odd number.").orDie }.catchAll(_ => myApp) def run = myApp } // Please enter a number: five // Please enter a number: 4 // You have entered an even number. ``` ## `ZIO#mapBoth` It takes two map functions, one for the error channel and the other for the success channel, and maps both sides of a ZIO effect: ```scala trait ZIO[-R, +E, +A] { def mapBoth[E2, B](f: E => E2, g: A => B): ZIO[R, E2, B] } ``` Here is a simple example: ```scala val result: ZIO[Any, String, Int] = Console.readLine.orDie.mapAttempt(_.toInt).mapBoth( _ => "non-integer input", n => Math.abs(n) ) ``` --- ## Merging the Error Channel into the Success Channel With `ZIO#merge` we can merge the error channel into the success channel: ```scala val merged : ZIO[Any, Nothing, String] = ZIO.fail("Oh uh!") // ZIO[Any, String, Nothing] .merge // ZIO[Any, Nothing, String] ``` If the error and success channels were of different types, it would choose the supertype of both. --- ## Rejecting Some Success Values We can reject some success values using the `ZIO#reject` operator: ```scala trait ZIO[-R, +E, +A] { def reject[E1 >: E](pf: PartialFunction[A, E1]): ZIO[R, E1, A] def rejectZIO[R1 <: R, E1 >: E]( pf: PartialFunction[A, ZIO[R1, E1, E1]] ): ZIO[R1, E1, A] } ``` If the `PartialFunction` matches, it will reject that success value and convert that to a failure, otherwise it will continue with the original success value: ```scala val myApp: ZIO[Any, String, Int] = Random .nextIntBounded(20) .reject { case n if n % 2 == 0 => s"even number rejected: $n" case 5 => "number 5 was rejected" } .debug ``` --- ## Tapping Errors Like [tapping for success values](../../core/zio/zio.md#tapping) ZIO has several operators for tapping error values. So we can peek into failures or underlying defects or causes: ```scala trait ZIO[-R, +E, +A] { def tapError[R1 <: R, E1 >: E](f: E => ZIO[R1, E1, Any]): ZIO[R1, E1, A] def tapErrorCause[R1 <: R, E1 >: E](f: Cause[E] => ZIO[R1, E1, Any]): ZIO[R1, E1, A] def tapErrorTrace[R1 <: R, E1 >: E](f: ((E, Trace)) => ZIO[R1, E1, Any]): ZIO[R1, E1, A] def tapDefect[R1 <: R, E1 >: E](f: Cause[Nothing] => ZIO[R1, E1, Any]): ZIO[R1, E1, A] def tapBoth[R1 <: R, E1 >: E](f: E => ZIO[R1, E1, Any], g: A => ZIO[R1, E1, Any]): ZIO[R1, E1, A] def tapEither[R1 <: R, E1 >: E](f: Either[E, A] => ZIO[R1, E1, Any]): ZIO[R1, E1, A] } ``` Let's try an example: ```scala object MainApp extends ZIOAppDefault { val myApp: ZIO[Any, NumberFormatException, Int] = Console.readLine .mapAttempt(_.toInt) .refineToOrDie[NumberFormatException] .tapError { e => ZIO.debug(s"user entered an invalid input: ${e}").when(e.isInstanceOf[NumberFormatException]) } def run = myApp } ``` --- ## Zooming In on Nested Values ## Option We can extract a value from a Some using `ZIO#some` and then we can unsome it again using `ZIO#unsome`: ```scala ZIO.attempt(Option("something")) // ZIO[Any, Throwable, Option[String]] .some // ZIO[Any, Option[Throwable], String] .unsome // ZIO[Any, Throwable, Option[String]] ``` ## Either With `Either` ZIO values, we can zoom in or out on the left or right side of an `Either`, as well as we can do the inverse and zoom out: ```scala val eitherEffect: ZIO[Any, Exception, Either[String, Int]] = ??? eitherEffect // ZIO[Any, Exception, Either[String, Int]] .left // ZIO[Any, Either[Exception, Int], String] .unleft // ZIO[Any, Exception, Either[String, Int]] eitherEffect // ZIO[Any, Exception, Either[String, Int]] .right // ZIO[Any, Either[String, Exception], Int] .unright // ZIO[Any, Exception, Either[String, Int]] ``` --- ## Catching ## Catching Failures If we want to catch and recover from all _typed error_ and effectfully attempt recovery, we can use the `ZIO#catchAll` operator: ```scala trait ZIO[-R, +E, +A] { def catchAll[R1 <: R, E2, A1 >: A](h: E => ZIO[R1, E2, A1]): ZIO[R1, E2, A1] } ``` We can recover from all errors while reading a file and then fallback to another operation: ```scala val z: ZIO[Any, IOException, Array[Byte]] = readFile("primary.json").catchAll(_ => readFile("backup.json")) ``` In the callback passed to `ZIO#catchAll`, we may return an effect with a different error type (or perhaps `Nothing`), which will be reflected in the type of effect returned by `ZIO#catchAll`. When using `ZIO#catchAll` operator, the match cases should be exhaustive. Remember our `validate` function again: ```scala sealed trait AgeValidationException extends Exception case class NegativeAgeException(age: Int) extends AgeValidationException case class IllegalAgeException(age: Int) extends AgeValidationException def validate(age: Int): ZIO[Any, AgeValidationException, Int] = if (age < 0) ZIO.fail(NegativeAgeException(age)) else if (age < 18) ZIO.fail(IllegalAgeException(age)) else ZIO.succeed(age) ``` In the following example, we covered all the cases for the `catchAll` operator: ```scala val result: ZIO[Any, Nothing, Int] = validate(20) .catchAll { case NegativeAgeException(age) => ZIO.debug(s"negative age: $age").as(-1) case IllegalAgeException(age) => ZIO.debug(s"illegal age: $age").as(-1) } ``` If we forget to catch all cases and the match fails, the original **failure** will be lost and replaced by a `MatchError` **defect**: ```scala object MainApp extends ZIOAppDefault { val result: ZIO[Any, Nothing, Int] = validate(15) .catchAll { case NegativeAgeException(age) => ZIO.debug(s"negative age: $age").as(-1) // case IllegalAgeException(age) => // ZIO.debug(s"illegal age: $age").as(-1) } def run = result } ``` Another important note about `ZIO#catchAll` is that this operator only can recover from _failures_. So it can't recover from defects or fiber interruptions. Let's try what happens if we `catchAll` on a dying effect: ```scala object MainApp extends ZIOAppDefault { val die: ZIO[Any, String, Nothing] = ZIO.dieMessage("Boom!") *> ZIO.fail("Oh uh!") def run = die.catchAll(_ => ZIO.unit) } // Output: // timestamp=2022-03-03T11:04:41.209169849Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.RuntimeException: Boom! // at .MainApp.die(MainApp.scala:6) // at .MainApp.run(MainApp.scala:8)" ``` Also, if we have a fiber interruption, we can't catch that using this operator: ```scala object MainApp extends ZIOAppDefault { val interruptedEffect: ZIO[Any, String, Nothing] = ZIO.interrupt *> ZIO.fail("Oh uh!") def run = interruptedEffect.catchAll(_ => ZIO.unit) } // Output: // timestamp=2022-03-03T11:10:15.573588420Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.InterruptedException: Interrupted by thread "zio-fiber-" // at .MainApp.die(MainApp.scala:6) // at .MainApp.run(MainApp.scala:8)" ``` If we want to catch and recover from only some types of exceptions and effectfully attempt recovery, we can use the `ZIO#catchSome` method: ```scala trait ZIO[-R, +E, +A] { def catchSome[R1 <: R, E1 >: E, A1 >: A](pf: PartialFunction[E, ZIO[R1, E1, A1]]): ZIO[R1, E1, A1] } ``` In the following example, we are only catching failure of type `FileNotFoundException`: ```scala val data: ZIO[Any, IOException, Array[Byte]] = readFile("primary.data").catchSome { case _ : FileNotFoundException => readFile("backup.data") } ``` ## Catching Defects Like catching failures, ZIO has two operators to catch _defects_: ```scala trait ZIO[-R, +E, +A] { def catchAllDefect[R1 <: R, E1 >: E, A1 >: A](h: Throwable => ZIO[R1, E1, A1]): ZIO[R1, E1, A1] def catchSomeDefect[R1 <: R, E1 >: E, A1 >: A](pf: PartialFunction[Throwable, ZIO[R1, E1, A1]]): ZIO[R1, E1, A1] } ``` Let's try the `ZIO#catchAllDefect` operator: ```scala ZIO.dieMessage("Boom!") .catchAllDefect { case e: RuntimeException if e.getMessage == "Boom!" => ZIO.debug("Boom! defect caught.") case _: NumberFormatException => ZIO.debug("NumberFormatException defect caught.") case _ => ZIO.debug("Unknown defect caught.") } ``` We should note that using these operators, we can only recover from a dying effect, and it cannot recover from a failure or fiber interruption. A defect is an error that cannot be anticipated in advance, and there is no way to respond to it. Our rule of thumb is to not recover defects since we don't know about them. We let them crash the application. Although, in some cases, we might need to reload a part of the application instead of killing the entire application. Assume we have written an application that can load plugins at runtime. During the runtime of the plugins, if a defect occurs, we don't want to crash the entire application; rather, we log all defects and then reload the plugin. ## Catching Causes So far, we have only studied how to catch _failures_ and _defects_. But what about _fiber interruptions_ or how about the specific combination of these errors? There are two ZIO operators useful for catching causes: ```scala trait ZIO[-R, +E, +A] { def catchAllCause[R1 <: R, E2, A1 >: A](h: Cause[E] => ZIO[R1, E2, A1]): ZIO[R1, E2, A1] def catchSomeCause[R1 <: R, E1 >: E, A1 >: A](pf: PartialFunction[Cause[E], ZIO[R1, E1, A1]]): ZIO[R1, E1, A1] } ``` With the help of the `ZIO#catchAllCause` operator we can catch all errors of an effect and recover from them: ```scala val exceptionalEffect = ZIO.attempt(???) exceptionalEffect.catchAllCause { case Cause.Empty => ZIO.debug("no error caught") case Cause.Fail(value, _) => ZIO.debug(s"a failure caught: $value") case Cause.Die(value, _) => ZIO.debug(s"a defect caught: $value") case Cause.Interrupt(fiberId, _) => ZIO.debug(s"a fiber interruption caught with the fiber id: $fiberId") case Cause.Stackless(cause: Cause.Die, _) => ZIO.debug(s"a stackless defect caught: ${cause.value}") case Cause.Stackless(cause: Cause[_], _) => ZIO.debug(s"an unknown stackless defect caught: ${cause.squashWith(identity)}") case Cause.Then(left, right) => ZIO.debug(s"two consequence causes caught") case Cause.Both(left, right) => ZIO.debug(s"two parallel causes caught") } ``` Additionally, there is a partial version of this operator called `ZIO#catchSomeCause`, which can be used when we don't want to catch all causes, but some of them. ## Catching Traces The two `ZIO#catchAllTrace` and `ZIO#catchSomeTrace` operators are useful to catch the typed error as well as stack traces of exceptional effects: ```scala trait ZIO[-R, +E, +A] { def catchAllTrace[R1 <: R, E2, A1 >: A]( h: ((E, Trace)) => ZIO[R1, E2, A1] ): ZIO[R1, E2, A1] def catchSomeTrace[R1 <: R, E1 >: E, A1 >: A]( pf: PartialFunction[(E, Trace), ZIO[R1, E1, A1]] ): ZIO[R1, E1, A1] } ``` In the below example, let's try to catch a failure on the line number 4: ```scala ZIO .fail("Oh uh!") .catchAllTrace { case ("Oh uh!", trace) if trace.toJava .map(_.getLineNumber) .headOption .contains(4) => ZIO.debug("caught a failure on the line number 4") case _ => ZIO.debug("caught other failures") } ``` ## Catching Non-Fatal We can use the `ZIO#catchAll` to recover from all non-fatal errors: ```scala trait ZIO[-R, +E, +A] { final def catchAll[R1 <: R, E2, A1 >: A]( h: E => ZIO[R1, E2, A1] )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E2, A1] } ``` In case of occurring any [fatal error](#catching-traces), it will die. ```scala openFile("data.json").catchAll(_ => openFile("backup.json")) ``` --- ## Fallback ## `ZIO#orElse` We can try one effect, or if it fails, try another effect with the `orElse` combinator: ```scala trait ZIO[-R, +E, +A] { def orElse[R1 <: R, E2, A1 >: A](that: => ZIO[R1, E2, A1]): ZIO[R1, E2, A1] } ``` Let's try an example: ```scala val primaryOrBackupData: ZIO[Any, IOException, Array[Byte]] = readFile("primary.data").orElse(readFile("backup.data")) ``` ## `ZIO#orElseEither` If the original effect fails, this operator tries another effect, and as a result, returns either: ```scala trait ZIO[-R, +E, +A] { def orElseEither[R1 <: R, E2, B](that: => ZIO[R1, E2, B]): ZIO[R1, E2, Either[A, B]] } ``` This operator is useful when the fallback effect has a different result type than the original effect. So this will unify both in the `Either[A, B]` data type. Here is an example usage of this operator: ```scala trait LocalConfig trait RemoteConfig def readLocalConfig: ZIO[Any, Throwable, LocalConfig] = ??? def readRemoteConfig: ZIO[Any, Throwable, RemoteConfig] = ??? val result: ZIO[Any, Throwable, Either[LocalConfig, RemoteConfig]] = readLocalConfig.orElseEither(readRemoteConfig) ``` ## `ZIO#orElseSucceed`/`ZIO#orElseFail` These two operators convert the original failure with constant succeed or failure values: ```scala trait ZIO[-R, +R, +E] { def orElseFail[E1](e1: => E1): ZIO[R, E1, A] def orElseSucceed[A1 >: A](a1: => A1): ZIO[R, Nothing, A1] } ``` The `ZIO#orElseFail` will always replace the original failure with the new one, so `E1` does not have to be a supertype of `E`. It is useful when we have `Unit` as an error, and we want to unify that with something else: ```scala sealed trait AgeValidationException extends Exception case class NegativeAgeException(age: Int) extends AgeValidationException case class IllegalAgeException(age: Int) extends AgeValidationException def validate(age: Int): ZIO[Any, AgeValidationException, Int] = { if (age < 0) ZIO.fail(NegativeAgeException(age)) else if (age < 18) ZIO.fail(IllegalAgeException(age)) else ZIO.succeed(age) } val result: ZIO[Any, String, Int] = validate(3).orElseFail("invalid age") ``` The `ZIO#orElseSucceed` will always replace the original failure with a success value so the resulting effect cannot fail. It is useful when we have a constant value that will work in case the effect fails: ```scala val result: ZIO[Any, Nothing, Int] = validate(3).orElseSucceed(0) ``` ## `ZIO#orElseOptional` When dealing with optional failure types, we might need to fall back to another effect when the failure value is `None`. This operator helps to do so: ```scala trait ZIO[-R, +E, +A] { def orElseOptional[R1 <: R, E1, A1 >: A]( that: => ZIO[R1, Option[E1], A1] )(implicit ev: E IsSubtypeOfError Option[E1]): ZIO[R1, Option[E1], A1] = } ``` In the following example, the `parseInt(" ")` fails with `None`, so then the fallback effect results in a zero: ```scala def parseInt(input: String): ZIO[Any, Option[String], Int] = input.toIntOption match { case Some(value) => ZIO.succeed(value) case None => if (input.trim.isEmpty) ZIO.fail(None) else ZIO.fail(Some(s"invalid non-integer input: $input")) } val result = parseInt(" ").orElseOptional(ZIO.succeed(0)).debug ``` ## `ZIO.firstSuccessOf`/`ZIO#firstSuccessOf` These two operators make it easy for a user to run an effect, and in case it fails, it will run a series of ZIO effects until one succeeds: ```scala object ZIO { def firstSuccessOf[R, R1 <: R, E, A]( zio: => ZIO[R, E, A], rest: => Iterable[ZIO[R1, E, A]] ): ZIO[R1, E, A] = } trait ZIO[-R, +E, +A] { final def firstSuccessOf[R1 <: R, E1 >: E, A1 >: A]( rest: => Iterable[ZIO[R1, E1, A1]] ): ZIO[R1, E1, A1] } ``` These methods use `orElse` to reduce the non-empty iterable of effects into a single effect. In the following example, we are trying to get the config from the master node, and if it fails, we will try successively to retrieve the config from the next available node: ```scala trait Config def remoteConfig(name: String): ZIO[Any, Throwable, Config] = ZIO.attempt(???) val masterConfig: ZIO[Any, Throwable, Config] = remoteConfig("master") val nodeConfigs: Seq[ZIO[Any, Throwable, Config]] = List("node1", "node2", "node3", "node4").map(remoteConfig) val config: ZIO[Any, Throwable, Config] = ZIO.firstSuccessOf(masterConfig, nodeConfigs) ``` --- ## Folding Scala's `Option` and `Either` data types have `fold`, which let us handle both failure and success at the same time. In a similar fashion, `ZIO` effects also have several methods that allow us to handle both failure and success. ## `ZIO#fold`/`ZIO#foldZIO` The first fold method, `ZIO#fold`, lets us non-effectfully handle both failure and success, by supplying a non-effectful handler for each case. The second fold method, `ZIO#foldZIO`, lets us effectfully handle both failure and success, by supplying an effectful (but still pure) handler for each case: ```scala trait ZIO[-R, +E, +A] { def fold[B]( failure: E => B, success: A => B ): ZIO[R, Nothing, B] def foldZIO[R1 <: R, E2, B]( failure: E => ZIO[R1, E2, B], success: A => ZIO[R1, E2, B] ): ZIO[R1, E2, B] } ``` Let's try an example: ```scala lazy val DefaultData: Array[Byte] = Array(0, 0) val primaryOrDefaultData: UIO[Array[Byte]] = readFile("primary.data").fold(_ => DefaultData, data => data) ``` We can ignore any failure and success values: ```scala val result: ZIO[Any, Nothing, Unit] = ZIO .fail("Uh oh!") // ZIO[Any, String, Int] .as(5) // ZIO[Any, String, Int] .fold(_ => (), _ => ()) // ZIO[Any, Nothing, Unit] ``` It is equivalent to use the `ZIO#ignore` operator instead: ```scala val result: ZIO[Any, Nothing, Unit] = ZIO.fail("Uh oh!").as(5).ignore ``` Now let's try the effectful version of the fold operation. In this example, in case of failure on reading from the primary file, we will fallback to another effectful operation which will read data from the secondary file: ```scala val primaryOrSecondaryData: IO[IOException, Array[Byte]] = readFile("primary.data").foldZIO( failure = _ => readFile("secondary.data"), success = data => ZIO.succeed(data) ) ``` Nearly all error handling methods are defined in terms of `foldZIO`, because it is both powerful and fast. In the following example, `foldZIO` is used to handle both failure and success of the `readUrls` method: ```scala val urls: UIO[Content] = readUrls("urls.json").foldZIO( error => ZIO.succeed(NoContent(error)), success => fetchContent(success) ) ``` It's important to note that both `ZIO#fold` and `ZIO#foldZIO` operators cannot catch fiber interruptions. So the following application will crash due to `InterruptedException`: ```scala object MainApp extends ZIOAppDefault { def run = (ZIO.interrupt *> ZIO.fail("Uh oh!")).fold(_ => (), _ => ()) } ``` And here is the output: ```scala timestamp=2022-02-24T13:41:01.696273024Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.InterruptedException: Interrupted by thread "zio-fiber-" at .MainApp.run(MainApp.scala:4)" ``` ## `ZIO#foldCause`/`ZIO#foldCauseZIO` This cause version of the `fold` operator is useful to access the full cause of the underlying fiber. So in case of failure, based on the exact cause, we can determine what to do: ```scala trait ZIO[-R, +E, +A] { def foldCause[B]( failure: Cause[E] => B, success: A => B ): ZIO[R, Nothing, B] def foldCauseZIO[R1 <: R, E2, B]( failure: Cause[E] => ZIO[R1, E2, B], success: A => ZIO[R1, E2, B] ): ZIO[R1, E2, B] } ``` Among the fold operators, these are the most powerful combinators. They can recover from any error, even fiber interruptions. In the following example, we are printing the proper message according to what cause occurred due to failure: ```scala val exceptionalEffect: ZIO[Any, Throwable, Unit] = ??? val myApp: ZIO[Any, IOException, Unit] = exceptionalEffect.foldCauseZIO( failure = { case Cause.Fail(value, _) => Console.printLine(s"failure: $value") case Cause.Die(value, _) => Console.printLine(s"cause: $value") case Cause.Interrupt(failure, _) => Console.printLine(s"${failure.threadName} interrupted!") case _ => Console.printLine("failed due to other causes") }, success = succeed => Console.printLine(s"succeeded with $succeed value") ) ``` When catching errors using this operator, if our cases were not exhaustive, we may receive a defect of the type `scala.MatchError` : ```scala object MainApp extends ZIOAppDefault { val exceptionalEffect: ZIO[Any, Throwable, Unit] = ZIO.interrupt val myApp: ZIO[Any, IOException, Unit] = exceptionalEffect.foldCauseZIO( failure = { case Cause.Fail(value, _) => ZIO.debug(s"failure: $value") case Cause.Die(value, _) => ZIO.debug(s"cause: ${value.toString}") // case Cause.Interrupt(failure, _) => ZIO.debug(s"${failure.threadName} interrupted!") }, success = succeed => ZIO.debug(s"succeeded with $succeed value") ) def run = myApp } ``` The output: ```scala timestamp=2022-02-24T11:05:40.241436257Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" scala.MatchError: Interrupt(Runtime(2,1645700739),Trace(Runtime(2,1645700739),Chunk(.MainApp.exceptionalEffect(MainApp.scala:6),.MainApp.myApp(MainApp.scala:9)))) (of class zio.Cause$Interrupt) at MainApp$.$anonfun$myApp$1(MainApp.scala:10) at zio.ZIO$TracedCont$$anon$33.apply(ZIO.scala:6167) at zio.ZIO$TracedCont$$anon$33.apply(ZIO.scala:6165) at zio.internal.FiberContext.runUntil(FiberContext.scala:885) at zio.internal.FiberContext.run(FiberContext.scala:115) at zio.internal.ZScheduler$$anon$1.run(ZScheduler.scala:151) at zio.internal.FiberContext.runUntil(FiberContext.scala:538)" ``` ## `ZIO#foldTraceZIO` This version of fold, provide us the facility to access the trace info of the failure: ```scala trait ZIO[-R, +E, +A] { def foldTraceZIO[R1 <: R, E2, B]( failure: ((E, Trace)) => ZIO[R1, E2, B], success: A => ZIO[R1, E2, B] )(implicit ev: CanFail[E]): ZIO[R1, E2, B] } ``` ```scala val result: ZIO[Any, Nothing, Int] = validate(5).foldTraceZIO( failure = { case (_: NegativeAgeException, trace) => ZIO.succeed(0).debug( "The entered age is negative\n" + s"trace info: ${trace.stackTrace.mkString("\n")}" ) case (_: IllegalAgeException, trace) => ZIO.succeed(0).debug( "The entered age in not legal\n" + s"trace info: ${trace.stackTrace.mkString("\n")}" ) }, success = s => ZIO.succeed(s) ) ``` Note that similar to `ZIO#fold` and `ZIO#foldZIO` this operator cannot recover from fiber interruptions. --- ## Retrying When we are building applications we want to be resilient in the face of a transient failure. This is where we need to retry to overcome these failures. There are a number of useful methods on the ZIO data type for retrying failed effects: ## `ZIO#retry` The most basic of these is `ZIO#retry`, which takes a `Schedule` and returns a new effect that will retry the first effect if it fails, according to the specified policy: ```scala trait ZIO[-R, +E, +A] { def retry[R1 <: R, S](policy: => Schedule[R1, E, S]): ZIO[R1, E, A] } ``` In this example, we try to read from a file. If we fail to do that, it will try five more times: ```scala val retriedOpenFile: ZIO[Any, IOException, Array[Byte]] = readFile("primary.data").retry(Schedule.recurs(5)) ``` ## `ZIO#retryN` In case of failure, a ZIO effect can be retried as many times as specified: ```scala val file = readFile("primary.data").retryN(5) ``` ## `ZIO#retryOrElse` The next most powerful function is `ZIO#retryOrElse`, which allows specification of a fallback to use, if the effect does not succeed with the specified policy: ```scala trait ZIO[-R, +E, +A] { def retryOrElse[R1 <: R, A1 >: A, S, E1]( policy: => Schedule[R1, E, S], orElse: (E, S) => ZIO[R1, E1, A1] ): ZIO[R1, E1, A1] = } ``` The `orElse` is the recovery function that has two inputs: 1. The last error message 2. Schedule output So based on these two values, we can decide what to do as the fallback operation. Let's try an example: ```scala object MainApp extends ZIOAppDefault { def run = Random .nextIntBounded(11) .flatMap { n => if (n < 9) ZIO.fail(s"$n is less than 9!").debug("failed") else ZIO.succeed(n).debug("succeeded") } .retryOrElse( policy = Schedule.recurs(5), orElse = (lastError, scheduleOutput: Long) => ZIO.debug(s"after $scheduleOutput retries, we couldn't succeed!") *> ZIO.debug(s"the last error message we received was: $lastError") *> ZIO.succeed(-1) ) .debug("the final result") } ``` ## `ZIO#retryOrElseEither` This operator is almost the same as the **`ZIO#retryOrElse`** except it will return either result of the original or the fallback operation: ```scala trait LocalConfig trait RemoteConfig def readLocalConfig: ZIO[Any, Throwable, LocalConfig] = ??? def readRemoteConfig: ZIO[Any, Throwable, RemoteConfig] = ??? val result: ZIO[Any, Throwable, Either[RemoteConfig, LocalConfig]] = readLocalConfig.retryOrElseEither( schedule0 = Schedule.fibonacci(1.seconds), orElse = (_, _: Duration) => readRemoteConfig ) ``` ## `ZIO#retryUntil`/`ZIO#retryUntilZIO` We can retry an effect until a condition on the error channel is satisfied: ```scala trait ZIO[-R, +E, +A] { def retryUntil(f: E => Boolean): ZIO[R, E, A] def retryUntilZIO[R1 <: R](f: E => URIO[R1, Boolean]): ZIO[R1, E, A] } ``` Assume we have defined the following remote service call: ```scala sealed trait ServiceError extends Exception case object TemporarilyUnavailable extends ServiceError case object DataCorrupted extends ServiceError def remoteService: ZIO[Any, ServiceError, Unit] = ??? ``` In the following example, we repeat the failed remote service call until we reach the `DataCorrupted` error: ```scala remoteService.retryUntil(_ == DataCorrupted) ``` To provide an effectful predicate we use the `ZIO#retryUntilZIO` operator. ## `ZIO#retryUntilEqual` Like the previous operator, it tries until its error is equal to the specified error: ```scala remoteService.retryUntilEquals(DataCorrupted) ``` ## `ZIO#retryWhile`/`ZIO#retryWhileZIO` Unlike the `ZIO#retryUntil` it will retry the effect while its error satisfies the specified predicate: ```scala trait ZIO[-R, +E, +A] { def retryWhile(f: E => Boolean): ZIO[R, E, A] def retryWhileZIO[R1 <: R](f: E => URIO[R1, Boolean]): ZIO[R1, E, A] } ``` In the following example, we repeat the failed remote service call while we have the `TemporarilyUnavailable` error: ```scala remoteService.retryWhile(_ == TemporarilyUnavailable) ``` To provide an effectful predicate we use the `ZIO#retryWhileZIO` operator. ## `ZIO#retryWhileEquals` Like the previous operator, it tries while its error is equal to the specified error: ```scala remoteService.retryWhileEquals(TemporarilyUnavailable) ``` --- ## Sandboxing We know that a ZIO effect may fail due to a failure, a defect, a fiber interruption, or a combination of these causes. So a ZIO effect may contain more than one cause. Using the `ZIO#sandbox` operator, we can sandbox all errors of a ZIO application, whether the cause is a failure, defect, or a fiber interruption or combination of these. This operator exposes the full cause of a ZIO effect into the error channel: ```scala trait ZIO[-R, +E, +A] { def sandbox: ZIO[R, Cause[E], A] } ``` We can use the `ZIO#sandbox` operator to uncover the full causes of an _exceptional effect_. So we can see all the errors that occurred as a type of `Cause[E]` at the error channel of the `ZIO` data type. So then we can use normal error-handling operators such as `ZIO#catchSome` and `ZIO#catchAll` operators: ```scala object MainApp extends ZIOAppDefault { val effect: ZIO[Any, String, String] = ZIO.succeed("primary result") *> ZIO.fail("Oh uh!") val myApp: ZIO[Any, Cause[String], String] = effect.sandbox.catchSome { case Cause.Interrupt(fiberId, _) => ZIO.debug(s"Caught interruption of a fiber with id: $fiberId") *> ZIO.succeed("fallback result on fiber interruption") case Cause.Die(value, _) => ZIO.debug(s"Caught a defect: $value") *> ZIO.succeed("fallback result on defect") case Cause.Fail(value, _) => ZIO.debug(s"Caught a failure: $value") *> ZIO.succeed("fallback result on failure") } val finalApp: ZIO[Any, String, String] = myApp.unsandbox.debug("final result") def run = finalApp } // Output: // Caught a failure: Oh uh! // final result: fallback result on failure ``` Using the `sandbox` operation we are exposing the full cause of an effect. So then we have access to the underlying cause in more detail. After handling exposed causes using `ZIO#catch*` operators, we can undo the `sandbox` operation using the `unsandbox` operation. It will submerge the full cause (`Cause[E]`) again: ```scala val effect: ZIO[Any, String, String] = ZIO.succeed("primary result") *> ZIO.fail("Oh uh!") effect // ZIO[Any, String, String] .sandbox // ZIO[Any, Cause[String], String] .catchSome(???) // ZIO[Any, Cause[String], String] .unsandbox // ZIO[Any, String, String] ``` There is another version of sandbox called `ZIO#sandboxWith`. This operator helps us to sandbox, then catch all causes, and then unsandbox back: ```scala trait ZIO[-R, +E, +A] { def sandboxWith[R1 <: R, E2, B](f: ZIO[R1, Cause[E], A] => ZIO[R1, Cause[E2], B]) } ``` Let's try the previous example using this operator: ```scala object MainApp extends ZIOAppDefault { val effect: ZIO[Any, String, String] = ZIO.succeed("primary result") *> ZIO.fail("Oh uh!") val myApp = effect.sandboxWith[Any, String, String] { e => e.catchSome { case Cause.Interrupt(fiberId, _) => ZIO.debug(s"Caught interruption of a fiber with id: $fiberId") *> ZIO.succeed("fallback result on fiber interruption") case Cause.Die(value, _) => ZIO.debug(s"Caught a defect: $value") *> ZIO.succeed("fallback result on defect") case Cause.Fail(value, _) => ZIO.debug(s"Caught a failure: $value") *> ZIO.succeed("fallback result on failure") } } def run = myApp.debug } // Output: // Caught a failure: Oh uh! // fallback result on failure ``` --- ## Timing out ## `ZIO#timeout` ZIO lets us timeout any effect using the `ZIO#timeout` method, which returns a new effect that succeeds with an `Option`. A value of `None` indicates the timeout elapsed before the effect completed. If an effect times out, then instead of continuing to execute in the background, it will be interrupted so no resources will be wasted. Assume we have the following effect: ```scala val myApp = for { _ <- ZIO.debug("start doing something.") _ <- ZIO.sleep(2.second) _ <- ZIO.debug("my job is finished!") } yield "result" ``` We should note that when we use the `ZIO#timeout` operator on the `myApp`, it doesn't return until one of the following situations happens: 1. The original effect returns before the timeout elapses so the output will be `Some` of the produced value by the original effect: ```scala object MainApp extends ZIOAppDefault { def run = myApp .timeout(3.second) .debug("output") .timed .map(_._1.toMillis / 1000) .debug("execution time of the whole program in second") } // Output: // start doing something. // my job is finished! // output: Some(result) // execution time of the whole program in second: 2 ``` 2. The original effect interrupted after the timeout elapses: - If the effect is interruptible it will be immediately interrupted, and finally, the timeout operation produces `None` value. ```scala mdoc:compile-only object MainApp extends ZIOAppDefault { def run = myApp .timeout(1.second) .debug("output") .timed .map(_._1.toMillis / 1000) .debug("execution time of the whole program in second") } // Output: // start doing something. // output: None // execution time of the whole program in second: 1 ``` - If the effect is uninterruptible it will be blocked until the original effect safely finished its work, and then the timeout operator produces the `None` value: ```scala mdoc:compile-only object MainApp extends ZIOAppDefault { def run = myApp .uninterruptible .timeout(1.second) .debug("output") .timed .map(_._1.toMillis / 1000) .debug("execution time of the whole program in second") } // Output: // start doing something. // my job is finished! // output: None // execution time of the whole program in second: 2 ``` Instead of waiting for the original effect to be interrupted, we can use `effect.disconnect.timeout` which first disconnects the effect's interruption signal before performing the timeout. By using this technique, we can return early after the timeout has passed and before an underlying effect has been interrupted. ```scala object MainApp extends ZIOAppDefault { def run = myApp .uninterruptible .disconnect .timeout(1.second) .debug("output") .timed .map(_._1.toMillis / 1000) .debug("execution time of the whole program in second") } ``` By using this technique, the original effect will be interrupted in the background. ## `ZIO#timeoutTo` This operator is similar to the previous one, but it also allows us to manually create the final result type: ```scala val delayedNextInt: ZIO[Any, Nothing, Int] = Random.nextIntBounded(10).delay(2.second) val r1: ZIO[Any, Nothing, Option[Int]] = delayedNextInt.timeoutTo(None)(Some(_))(1.seconds) val r2: ZIO[Any, Nothing, Either[String, Int]] = delayedNextInt.timeoutTo(Left("timeout"))(Right(_))(1.seconds) val r3: ZIO[Any, Nothing, Int] = delayedNextInt.timeoutTo(-1)(identity)(1.seconds) ``` ## `ZIO#timeoutFail`/`ZIO#timeoutFailCause` In case of elapsing the timeout, we can produce a particular error message: ```scala val r1: ZIO[Any, TimeoutException, Int] = delayedNextInt.timeoutFail(new TimeoutException)(1.second) val r2: ZIO[Any, Nothing, Int] = delayedNextInt.timeoutFailCause(Cause.die(new Error("timeout")))(1.second) ``` --- ## Sequential and Parallel Errors A simple and regular ZIO application usually fails with one error, which is the first error encountered by the ZIO runtime: ```scala object MainApp extends ZIOAppDefault { val fail = ZIO.fail("Oh uh!") val die = ZIO.dieMessage("Boom!") val interruption = ZIO.interrupt def run = (fail <*> die) *> interruption } ``` This application will fail with the first error which is "Oh uh!": ```scala timestamp=2022-03-09T09:50:22.067072131Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.String: Oh uh! at .MainApp.fail(MainApp.scala:4)" ``` In some cases, we may run into multiple errors. When we perform parallel computations, the application may fail due to multiple errors: ```scala object MainApp extends ZIOAppDefault { def run = ZIO.fail("Oh!") <&> ZIO.fail("Uh!") } ``` If we run this application, we can see two exceptions in two different fibers that caused the failure (`zio-fiber-0` and `zio-fiber-14`): ```scala timestamp=2022-03-09T08:05:48.703035927Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-13" java.lang.String: Oh! at .MainApp.run(MainApp.scala:4) Exception in thread "zio-fiber-14" java.lang.String: Uh! at .MainApp.run(MainApp.scala:4)" ``` ZIO has a combinator called `ZIO#parallelErrors` that exposes all parallel failure errors in the error channel: ```scala val result: ZIO[Any, ::[String], Nothing] = (ZIO.fail("Oh uh!") <&> ZIO.fail("Oh Error!")).parallelErrors ``` Note that this operator is only for failures, not defects or interruptions. Also, when we work with resource-safety operators like `ZIO#ensuring` we can have multiple sequential errors. Why? because regardless of the original effect has any errors or not, the finalizer is uninterruptible. So the finalizer will be run. Unless the finalizer should be an unexceptional effect (`URIO`), it may die because of a defect. Therefore, it creates multiple sequential errors: ```scala object MainApp extends ZIOAppDefault { def run = ZIO.fail("Oh uh!").ensuring(ZIO.dieMessage("Boom!")) } ``` When we run this application, we can see that the original failure (`Oh uh!`) was suppressed by another defect (`Boom!`): ```scala timestamp=2022-03-09T08:30:56.563179230Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.String: Oh uh! at .MainApp.run(MainApp.scala:4) Suppressed: java.lang.RuntimeException: Boom! at .MainApp.run(MainApp.scala:4)" ``` --- ## Typed Errors Guarantees **Typed errors don't guarantee the absence of defects and interruptions.** Having an effect of type `ZIO[R, E, A]`, means it can fail because of some failure of type `E`, but it doesn't mean it can't die or be interrupted. So the error channel is only for `failure` errors. In the following example, the type of the `validateNonNegativeNumber` function is `ZIO[Any, String, Int]` which denotes it is a typed exceptional effect. It can fail of type `String` but it still can die with the type of `NumberFormatException` defect: ```scala def validateNonNegativeNumber(input: String): ZIO[Any, String, Int] = input.toIntOption match { case Some(value) if value >= 0 => ZIO.succeed(value) case Some(other) => ZIO.fail(s"the entered number is negative: $other") case None => ZIO.die( new NumberFormatException( s"the entered input is not in the correct number format: $input" ) ) } ``` Also, its underlying fiber can be interrupted without affecting the type of the error channel: ```scala val myApp: ZIO[Any, String, Int] = for { f <- validateNonNegativeNumber("5").fork _ <- f.interrupt r <- f.join } yield r ``` Therefore, if we run the `myApp` effect, it will be interrupted before it gets the chance to finish. --- ## Defects By providing a `Throwable` value to the `ZIO.die` constructor, we can describe a dying effect: ```scala object ZIO { def die(t: => Throwable): ZIO[Any, Nothing, Nothing] } ``` Here is an example of such effect, which will die because of encountering _divide by zero_ defect: ```scala val dyingEffect: ZIO[Any, Nothing, Nothing] = ZIO.die(new ArithmeticException("divide by zero")) ``` The result is the creation of a ZIO effect whose error channel and success channel are both `Nothing`. In other words, this effect cannot fail and does not produce anything. Instead, it is an effect describing a _defect_ or an _unexpected error_. Let's see what happens if we run this effect: ```scala object MainApp extends ZIOAppDefault { def run = ZIO.die(new ArithmeticException("divide by zero")) } ``` If we run this effect, ZIO runtime will print the stack trace that belongs to this defect. So, here is the output: ```scala timestamp=2022-02-16T13:02:44.057191215Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.ArithmeticException: divide by zero at MainApp$.$anonfun$run$1(MainApp.scala:4) at zio.ZIO$.$anonfun$die$1(ZIO.scala:3384) at zio.internal.FiberContext.runUntil(FiberContext.scala:255) at zio.internal.FiberContext.run(FiberContext.scala:115) at zio.internal.ZScheduler$$anon$1.run(ZScheduler.scala:151) at .MainApp.run(MainApp.scala:4)" ``` The `ZIO.die` constructor is used to manually describe a dying effect because of a defect inside the code. For example, assume we want to write a `divide` function that takes two numbers and divides the first number by the second. We know that the `divide` function is not defined for zero dominators. Therefore, we should signal an error if division by zero occurs. We have two choices to implement this function using the ZIO effect: 1. We can divide the first number by the second, and if the second number was zero, we can fail the effect using `ZIO.fail` with the `ArithmeticException` failure value: ```scala def divide(a: Int, b: Int): ZIO[Any, ArithmeticException, Int] = if (b == 0) ZIO.fail(new ArithmeticException("divide by zero")) else ZIO.succeed(a / b) ``` 2. We can divide the first number by the second. In the case of zero for the second number, we use `ZIO.die` to kill the effect by sending a signal of `ArithmeticException` as a defect: ```scala def divide(a: Int, b: Int): ZIO[Any, Nothing, Int] = if (b == 0) ZIO.die(new ArithmeticException("divide by zero")) // Unexpected error else ZIO.succeed(a / b) ``` So what is the difference between these two approaches? Let's compare the function signature: ```scala def divide(a: Int, b: Int): ZIO[Any, ArithmeticException, Int] // using ZIO.fail def divide(a: Int, b: Int): ZIO[Any, Nothing, Int] // using ZIO.die ``` 1. The first approach, models the _divide by zero_ error by _failing_ the effect. We call these failures _expected errors_ or _typed error_. 2. While the second approach models the _divide by zero_ error by _dying_ the effect. We call these kinds of errors _unexpected errors_, _defects_ or _untyped errors_. We use the first method when we are handling errors as we expect them, and thus we know how to handle them. In contrast, the second method is used when we aren't expecting those errors in our domain, and we don't know how to handle them. Therefore, we use the _let it crash_ philosophy. In the second approach, we can see that the `divide` function indicates that it cannot fail because it's error channel is `Nothing`. However, it doesn't mean that this function hasn't any defects. ZIO defects are not typed, so they cannot be seen in type parameters. Note that to create an effect that will die, we shouldn't throw an exception inside the `ZIO.die` constructor, although it works. Instead, the idiomatic way of creating a dying effect is to provide a `Throwable` value into the `ZIO.die` constructor: ```scala val defect1 = ZIO.die(new ArithmeticException("divide by zero")) // recommended val defect2 = ZIO.die(throw new ArithmeticException("divide by zero")) // not recommended ``` Also, if we import a code that may throw an exception, all the exceptions will be translated to the ZIO defect: ```scala val defect3 = ZIO.succeed(throw new Exception("boom!")) ``` Therefore, in the second approach of the `divide` function, we do not require to manually die the effect in case of the _dividing by zero_, because the JVM itself throws an `ArithmeticException` when the denominator is zero. When we import any code into the `ZIO` effect, any exception is thrown inside that code will be translated to _ZIO defects_ by default. So the following program is the same as the previous example: ```scala def divide(a: Int, b: Int): ZIO[Any, Nothing, Int] = ZIO.succeed(a / b) ``` Another important note is that if we `map`/`flatMap` a ZIO effect and then accidentally throw an exception inside the map operation, that exception will be translated to a ZIO defect: ```scala val defect4 = ZIO.succeed(???).map(_ => throw new Exception("Boom!")) val defect5 = ZIO.attempt(???).map(_ => throw new Exception("Boom!")) ``` --- ## Failures When writing ZIO application, we can model a failure, using the `ZIO.fail` constructor: ```scala trait ZIO { def fail[E](error: => E): ZIO[Any, E, Nothing] } ``` Let's try to model some failures using this constructor: ```scala val f1: ZIO[Any, String, Nothing] = ZIO.fail("Oh uh!") val f2: ZIO[Any, String, Int] = ZIO.succeed(5) *> ZIO.fail("Oh uh!") ``` Now, let's try to run a failing effect and see what happens: ```scala object MainApp extends ZIOAppDefault { def run = ZIO.succeed(5) *> ZIO.fail("Oh uh!") } ``` This will crash the application and print the following stack trace: ```scala timestamp=2022-03-08T17:55:50.002161369Z level=ERROR thread=#zio-fiber-0 message="Exception in thread "zio-fiber-2" java.lang.String: Oh uh! at .MainApp.run(MainApp.scala:4)" ``` We can also model a failure using `Exception`: ``` val f3: ZIO[Any, Exception, Nothing] = ZIO.fail(new Exception("Oh uh!")) ``` Or using user-defined failure types (domain errors): ```scala case class NegativeNumberException(msg: String) extends Exception(msg) def validateNonNegative(input: Int): ZIO[Any, NegativeNumberException, Int] = if (input < 0) ZIO.fail(NegativeNumberException(s"entered negative number: $input")) else ZIO.succeed(input) ``` In the above examples, we can see that the type of the `validateNonNegative` function is `ZIO[Any, NegativeNumberException, Int]`. It means this is an exceptional effect, which may fail with the type of `NegativeNumberException`. The `ZIO.fail` constructor is somehow the moral equivalent of `throw` for pure codes. We will discuss this [further](../declarative.md). --- ## Fatal Errors In ZIO on the JVM platform, the `VirtualMachineError` and all its subtypes are the only errors considered fatal by the ZIO runtime. So if during the running application, the JVM throws any of these errors like `StackOverflowError`, the ZIO runtime considers it as a catastrophic fatal error. So it will interrupt the whole application immediately without safe resource interruption. None of the `ZIO#catchAll` and `ZIO#catchAllDefects` can catch these fatal errors. At most, if we set the `Runtime.setReportFatal`, the application will log the stack trace before interrupting the entire application. Here is an example of manually creating a fatal error. Although we are ignoring all expected and unexpected errors, the fatal error interrupts the whole application: ```scala object MainApp extends ZIOAppDefault { def run = ZIO .attempt( throw new StackOverflowError( "The call stack pointer exceeds the stack bound." ) ) .catchAll(_ => ZIO.unit) // ignoring all expected errors .catchAllDefect(_ => ZIO.unit) // ignoring all unexpected errors } ``` The output will be something like this: ```scala java.lang.StackOverflowError: The call stack pointer exceeds the stack bound. at zio.examples.MainApp$.$anonfun$run$1(MainApp.scala:10) at zio.ZIO$.liftedTree1$1(ZIO.scala:2603) at zio.ZIO$.$anonfun$attempt$1(ZIO.scala:2603) at zio.ZIO$.$anonfun$isFatalWith$1(ZIO.scala:3571) at zio.internal.FiberContext.runUntil(FiberContext.scala:410) at zio.internal.FiberContext.run(FiberContext.scala:111) at zio.Runtime.unsafeRunWithRefs(Runtime.scala:400) ... **** WARNING **** Catastrophic error encountered. Application not safely interrupted. Resources may be leaked. Check the logs for more details and consider overriding `Runtime.reportFatal` to capture context. ``` Note that we can change the default way to report fatal errors using `Runtime#reportFatal` or the `Runtime.setReportFatal` layer. --- ## Three Types of Errors We should consider three types of errors when writing ZIO applications: 1. **[Failures](failures.md)** are expected errors. We use `ZIO.fail` to model failures. As they are expected, we know how to handle them. We should handle these errors and prevent them from propagating throughout the call stack. 2. **[Defects](defects.md)** are unexpected errors. We use `ZIO.die` to model a defect. As they are not expected, we need to propagate them through the application stack, until in the upper layers one of the following situations happens: - In one of the upper layers, it makes sense to expect these errors. So we will convert them to failure, and then they can be handled. - None of the upper layers will catch these errors, so it will finally crash the whole application. 3. **[Fatals](fatals.md)** are catastrophic unexpected errors. When they occur we should kill the application immediately without propagating the error furthermore. At most, we might need to log the error and print its call stack. --- ## Fiber To perform an effect without blocking the current process, we can use fibers, which are a lightweight concurrency mechanism. We can `fork` any `ZIO[R, E, A]` to immediately yield an `URIO[R, Fiber[E, A]]`. The provided `Fiber` can be used to `join` the fiber, which will resume on production of the fiber's value, or to `interrupt` the fiber, which immediately terminates the fiber and safely releases all resources acquired by the fiber. ```scala val analyzed = for { fiber1 <- analyzeData(data).fork // IO[E, Analysis] fiber2 <- validateData(data).fork // IO[E, Boolean] // Do other stuff valid <- fiber2.join _ <- if (!valid) fiber1.interrupt else ZIO.unit analyzed <- fiber1.join } yield analyzed ``` ## Lifetime of Child Fibers When we fork fibers, depending on how we fork them we can have four different lifetime strategies for the child fibers: 1. **Fork With Automatic Supervision**— If we use the ordinary `ZIO#fork` operation, the child fiber will be automatically supervised by the parent fiber. The lifetime child fibers are tied to the lifetime of their parent fiber. This means that these fibers will be terminated either when they end naturally, or when their parent fiber is terminated. 3. **Fork in Global Scope (Daemon)**— Sometimes we want to run long-running background fibers that aren't tied to their parent fiber, and also we want to fork them in a global scope. Any fiber that is forked in global scope will become daemon fiber. This can be achieved by using the `ZIO#forkDaemon` operator. As these fibers have no parent, they are not supervised, and they will be terminated when they end naturally, or when our application is terminated. 4. **Fork in Local Scope**— Sometimes, we want to run a background fiber that isn't tied to its parent fiber, but we want to live that fiber in the local scope. We can fork fibers in the local scope by using `ZIO#forkScoped`. Such fibers can outlive their parent fiber (so they are not supervised by their parents), and they will be terminated when their life end or their local scope is closed. 5. **Fork in Specific Scope**— This is similar to the previous strategy, but we can have more fine-grained control over the lifetime of the child fiber by forking it in a specific scope. We can do this by using the `ZIO#forkIn` operator. :::note Forking with **automatic supervision** is the _default strategy_. When we use the `ZIO#fork` method, the lifetime of child fibers is tied to their parent fiber. However, sometimes we don't want this behavior. Instead, we use three other alternatives. ::: :::note Managing Fiber Lifetime Using `Scope` Data Type The second and third strategies are required to work with the `Scope` data type. A contextual data type that describes a resource's lifetime, in this case, the fiber's lifetime. To learn more about `Scope` we have a [separate section](../resource/scope.md) on it. ::: ### Fork with Automatic Supervision ZIO uses a **structured concurrency** model where fiber lifetimes are cleanly nested. The lifetime of a fiber depends on the lifetime of its parent fiber. To illustrate this, let's look at some examples: 1. In the following example, we have two fibers. The first fiber is responsible for running the first and last debug tasks. It is also responsible for creating the second fiber. The second fiber is a task that forked from the first fiber and will never produce anything: ```scala object MainApp extends ZIOAppDefault { def run = for { _ <- ZIO.debug(s"Application started!") _ <- ZIO.never.onInterrupt(_ => ZIO.debug(s"The child fiber interrupted!")).fork _ <- ZIO.debug(s"Application finished!") } yield () } ``` In this example, the child fiber will be interrupted when the parent fiber is finished (successfully or interrupted). :::caution The example above is just for educational purposes. In a real application, the `onInterrupt` callback is not guaranteed to be called just before the parent fiber finishes its execution. It is possible that the parent fiber will be finished just before the `onInterrupt` callback is called. So this example is the simplified version of the following example: ```scala object MainApp extends ZIOAppDefault { def run = ZIO.fiberIdWith { parent => for { _ <- ZIO.debug(s"fiber-${parent.id} Application started!") latch <- Promise.make[Nothing, Unit] _ <- ZIO.fiberIdWith { child => (latch.succeed(()) *> ZIO.never).onInterrupt(_ => ZIO.debug(s"fiber-${child.id} The child fiber interrupted!") ) }.fork _ <- latch.await _ <- ZIO.debug(s"fiber-${parent.id} Application finished!") } yield () } } ``` In this version, other than making sure that the `onInterrupt` callback is called, we also added fiber ids to the debug messages. Here is the output of the above example: ``` fiber-6 Application started! fiber-6 Application finished! fiber-7 The child fiber interrupted! ``` ::: 2. Here is another example. In this case, the `foo` fiber creates the `bar` fiber. The `bar` fiber has a long-running task that never finishes. ZIO guarantees that the `bar` fiber will not outlive the `foo` fiber: ```scala object MainApp extends ZIOAppDefault { val barJob: ZIO[Any, Nothing, Long] = ZIO .debug("Bar: still running!") .repeat(Schedule.fixed(1.seconds)) val fooJob: ZIO[Any, Nothing, Unit] = for { _ <- ZIO.debug("Foo: started!") _ <- barJob.fork _ <- ZIO.sleep(3.seconds) _ <- ZIO.debug("Foo: finished!") } yield () def run = for { f <- fooJob.fork _ <- f.join } yield () } ``` The output of the above program is: ``` Foo: started! Bar: still running! Bar: still running! Bar: still running! Foo: finished! ``` This pattern can be applied to any nested level of fibers. ### Fork in Global Scope (Daemon) Using `ZIO#forkDaemon` we can create a daemon fiber from a `ZIO` effect. Its lifetime is tied to the global scope. So if the parent fiber terminates, the daemon fiber will not be terminated. It will only will be terminated when the global scope is closed, or its life end naturaly. ```scala object MainApp extends ZIOAppDefault { val barJob: ZIO[Any, Nothing, Long] = ZIO .debug("Bar: still running!") .repeat(Schedule.fixed(1.seconds)) val fooJob: ZIO[Any, Nothing, Unit] = for { _ <- ZIO.debug("Foo: started!") _ <- barJob.forkDaemon _ <- ZIO.sleep(3.seconds) _ <- ZIO.debug("Foo: finished!") } yield () def run = for { f <- fooJob.fork _ <- ZIO.sleep(5.seconds) } yield () } ``` If we run the above program, we will see the following output which shows that while the lifetime of the `foo` fiber ends after 3 seconds, the daemon fiber (`bar`) is still running until the global scope is closed: ``` Foo: started! Bar: still running! Bar: still running! Bar: still running! Foo: finished! Bar: still running! Bar: still running! ``` Even if we interrupt the `foo` fiber, the daemon fiber (`foo`) will not be interrupted: ```scala object MainApp2 extends ZIOAppDefault { val barJob: ZIO[Any, Nothing, Long] = ZIO .debug("Bar: still running!") .repeat(Schedule.fixed(1.seconds)) val fooJob: ZIO[Any, Nothing, Unit] = (for { _ <- ZIO.debug("Foo: started!") _ <- barJob.forkDaemon _ <- ZIO.sleep(3.seconds) _ <- ZIO.debug("Foo: finished!") } yield ()).onInterrupt(_ => ZIO.debug("Foo: interrupted!")) def run = for { f <- fooJob.fork _ <- ZIO.sleep(2.seconds) _ <- f.interrupt _ <- ZIO.sleep(3.seconds) } yield () } ``` The output: ``` Foo: started! Bar: still running! Bar: still running! Foo: interrupted! Bar: still running! Bar: still running! Bar: still running! Bar: still running! ``` ### Fork in Local Scope Sometimes we want to attach fiber to a local scope. In such cases, we can use the `ZIO#forkScoped` operator. By using this operator, the lifetime of the forked fiber can be outlived the lifetime of its parent fiber, and it will be terminated when the local scope is closed: ```scala object MainApp extends ZIOAppDefault { val barJob: ZIO[Any, Nothing, Long] = ZIO .debug("Bar: still running!") .repeat(Schedule.fixed(1.seconds)) val fooJob: ZIO[Scope, Nothing, Unit] = (for { _ <- ZIO.debug("Foo: started!") _ <- barJob.forkScoped _ <- ZIO.sleep(2.seconds) _ <- ZIO.debug("Foo: finished!") } yield ()).onInterrupt(_ => ZIO.debug("Foo: interrupted!")) def run = for { _ <- ZIO.scoped { for { _ <- ZIO.debug("Local scope started!") _ <- fooJob.fork _ <- ZIO.sleep(5.seconds) _ <- ZIO.debug("Leaving the local scope!") } yield () } _ <- ZIO.debug("Do something else and sleep for 10 seconds") _ <- ZIO.sleep(10.seconds) _ <- ZIO.debug("Application exited!") } yield () } ``` In the above example, the `bar` fiber forked in the local scope has bigger lifetime than its parent fiber (`foo`). So, when its parent fiber (`foo`) is terminated, the `bar` fiber still running in the local scope until the local scope is closed. Let's see the output: ``` Local scope started! Foo: started! Bar: still running! Bar: still running! Foo: finished! Bar: still running! Bar: still running! Bar: still running! Leaving the local scope! Do something else and sleep for 10 seconds Application exited! ``` ### Fork in Specific Scope There are some cases where we need more fine-grained control, so we want to fork a fiber in a specific scope. We can use the `ZIO#forkIn` operator which takes the target scope as an argument: ```scala object MainApp extends ZIOAppDefault { def run = ZIO.scoped { for { scope <- ZIO.scope _ <- ZIO.scoped { for { _ <- ZIO .debug("Still running ...") .repeat(Schedule.fixed(1.second)) .forkIn(scope) _ <- ZIO.sleep(3.seconds) _ <- ZIO.debug("The innermost scope is about to be closed.") } yield () } _ <- ZIO.sleep(5.seconds) _ <- ZIO.debug("The outer scope is about to be closed.") } yield () } } ``` The output: ``` Still running ... Still running ... Still running ... The innermost scope is about to be closed. Still running ... Still running ... Still running ... Still running ... Still running ... Still running ... The outer scope is about to be closed. ``` ## Background Processes and Layers Sometimes, we want to create a layer that has a background process attached to the global scope. For example, think of a Kafka layer that has a background process that is constantly reading/writing messages from/to Kafka topics. In such situations, we can create a layer from such a background process. In the following example, even though we have provided the `layer` locally to one part of our application, the background process inside the layer is still running in the global scope: ```scala object MainApp extends ZIOAppDefault { val layer: ZLayer[Scope, Nothing, Int] = ZLayer.fromZIO { ZIO .debug("Still running ...") .repeat(Schedule.fixed(1.second)) .forkDaemon .as(42) } def run = for { _ <- ZIO.service[Int].provideLayer(layer) *> ZIO.debug("Int layer provided") _ <- ZIO.sleep(5.seconds) } yield () } ``` Output: ``` Still running ... int layer provided Still running ... Still running ... Still running ... Still running ... Still running ... ``` ## Operations ### fork and join Whenever we need to start a fiber, we have to `fork` an effect to get a new fiber. This is similar to the `start` method on Java thread or submitting a new thread to the thread pool in Java, it is the same idea. Also, joining is a way of waiting for that fiber to compute its value. We are going to wait until it's done and receive its result. :::note Fibers (including those created with `forkDaemon`) **inherit the interruptibility status of their parent**. In other words, if the parent effect is currently running uninterruptibly then the child fiber will also be uninterruptible even calling `child.interrupt` will have no effect. To ensure a forked fiber is interruptible while preserving the parent’s uninterruptibility, use `ZIO.uninterruptibleMask`. For example, this code hangs because the child inherited uninterruptibility using `fib.interrupt`: ```scala val parent = ZIO.uninterruptible { for { _ <- ZIO.logInfo("Parent is uninterruptible") fib <- ZIO.never.fork _ <- ZIO.logInfo("Attempting to interrupt the child in 5 seconds...") _ <- ZIO.sleep(5.seconds) _ <- fib.interrupt *> ZIO.logInfo("Interrupt invoked!") // <— this will hang: child inherited uninterruptibility } yield () } ``` Using `ZIO.uninterruptibleMask` at the top level keeps the parent uninterruptible. Inside the mask, calling `restore(ZIO.never)` runs that effect as interruptible so the forked fiber becomes interruptible even though its parent is not. For example: ```scala val parentInterruptibleChild = ZIO.uninterruptibleMask { restore => for { _ <- ZIO.logInfo("Parent is uninterruptible") fib <- restore(ZIO.never).fork // <— child is now interruptible _ <- ZIO.logInfo("Attempting to interrupt the child in 5 seconds...") _ <- ZIO.sleep(5.seconds) _ <- fib.interrupt *> ZIO.logInfo("Interrupt invoked!") } yield () } ``` ::: In the following example, we create a separate fiber to output a delayed print message and then wait for that fiber to succeed with a value: ```scala for { fiber <- (ZIO.sleep(3.seconds) *> printLine("Hello, after 3 second") *> ZIO.succeed(10)).fork _ <- printLine(s"Hello, World!") res <- fiber.join _ <- printLine(s"Our fiber succeeded with $res") } yield () ``` ### interrupt Whenever we want to get rid of our fiber, we can simply call `interrupt` on that. The interrupt operation does not resume until the fiber has completed or has been interrupted and all its finalizers have been run. These precise semantics allow construction of programs that do not leak resources. ### await To inspect whether our fiber succeeded or failed, we can call `await` on that fiber. This call will wait for that fiber to terminate, and it will give us back the fiber's value as an `Exit`. That exit value could be failure or success: ```scala for { b <- Random.nextBoolean fiber <- (if (b) ZIO.succeed(10) else ZIO.fail("The boolean was not true")).fork exitValue <- fiber.await _ <- exitValue match { case Exit.Success(value) => printLine(s"Fiber succeeded with $value") case Exit.Failure(cause) => printLine(s"Fiber failed") } } yield () ``` `await` is similar to `join`, but they react differently to errors and interruption: `await` always succeeds with `Exit` information, even if the fiber fails or is interrupted. In contrast to that, `join` on a fiber that fails will itself fail with the same error as the fiber, and `join` on a fiber that is interrupted will itself become interrupted. ### Parallelism To execute actions in parallel, the `zipPar` method can be used: ```scala def bigCompute(m1: Matrix, m2: Matrix, v: Matrix): UIO[Matrix] = for { t <- computeInverse(m1).zipPar(computeInverse(m2)) (i1, i2) = t r <- applyMatrices(i1, i2, v) } yield r ``` The `zipPar` combinator has resource-safe semantics. If one computation fails, the other computation will be interrupted, to prevent wasting resources. ### Racing Two actions can be *raced*, which means they will be executed in parallel, and the value of the first action that completes successfully will be returned. ```scala fib(100) race fib(200) ``` The `race` combinator is resource-safe, which means that if one of the two actions returns a value, the other one will be interrupted, to prevent wasting resources. The `race` and even `zipPar` combinators are a specialization of a much-more powerful combinator called `raceWith`, which allows executing user-defined logic when the first of two actions succeeds. On the JVM, fibers will use threads, but will not consume *unlimited* threads. Instead, fibers yield cooperatively during periods of high-contention. ```scala def fib(n: Int): UIO[Int] = if (n <= 1) { ZIO.succeed(1) } else { for { fiber1 <- fib(n - 2).fork fiber2 <- fib(n - 1).fork v2 <- fiber2.join v1 <- fiber1.join } yield v1 + v2 } ``` ## Error Model The `ZIO` error model is simple, consistent, permits both typed errors and termination, and does not violate any laws in the `Functor` hierarchy. A `ZIO[R, E, A]` value may only raise errors of type `E`. These errors are recoverable by using the `either` method. The resulting effect cannot fail, because the failure case has been exposed as part of the `Either` success case. ```scala val error: Task[String] = ZIO.fail(new RuntimeException("Some Error")) val errorEither: ZIO[Any, Nothing, Either[Throwable, String]] = error.either ``` Separately from errors of type `E`, a fiber may be terminated for the following reasons: * **The fiber self-terminated or was interrupted by another fiber**. The "main" fiber cannot be interrupted because it was not forked from any other fiber. * **The fiber failed to handle some error of type `E`**. This can happen only when an `ZIO.fail` is not handled. For values of type `UIO[A]`, this type of failure is impossible. * **The fiber has a defect that leads to a non-recoverable error**. There are only two ways this can happen: 1. A partial function is passed to a higher-order function such as `map` or `flatMap`. For example, `io.map(_ => throw e)`, or `io.flatMap(a => throw e)`. The solution to this problem is to not to pass impure functions to purely functional libraries like ZIO, because doing so leads to violations of laws and destruction of equational reasoning. 2. Error-throwing code was embedded into some value via `ZIO.succeed`, etc. For importing partial effects into `ZIO`, the proper solution is to use a method such as `ZIO.attempt`, which safely translates exceptions into values. When a fiber is terminated, the reason for the termination, expressed as a `Throwable`, is passed to the fiber's supervisor, which may choose to log, print the stack trace, restart the fiber, or perform some other action appropriate to the context. A fiber cannot stop its own interruption. However, all finalizers will be run during termination, even when some finalizers throw non-recoverable errors. Errors thrown by finalizers are passed to the fiber's supervisor. There are no circumstances in which any errors will be "lost", which makes the `ZIO` error model more diagnostic-friendly than the `try`/`catch`/`finally` construct that is baked into both Scala and Java, which can easily lose errors. ## Fiber Interruption In Java, a thread can be interrupted via `Thread#interrupt` from another thread, but it may refuse the interruption request and continue processing. Unlike Java, in ZIO when a fiber interrupts another fiber, we know that the interruption occurs, and it always works. When working with ZIO fibers, we should consider these notes about fiber interruptions: ### Interruptible/Uninterruptible Regions All fibers are interruptible by default. To make an effect uninterruptible we can use `Fiber#uninterruptible`, `ZIO#uninterruptible` or `ZIO.uninterruptible`. ZIO provides also the reverse direction of these methods to make an uninterruptible effect interruptible. ```scala for { fiber <- Clock.currentDateTime .flatMap(time => printLine(time)) .schedule(Schedule.fixed(1.seconds)) .uninterruptible .fork _ <- fiber.interrupt // Runtime stuck here and does not go further } yield () ``` Note that there is no way to stop interruption. We can only delay it, by making an effect uninterruptible. ### Fiber Finalization on Interruption When a fiber has done its work or has been interrupted, the finalizer of that fiber is guaranteed to be executed: ```scala for { fiber <- printLine("Working on the first job") .schedule(Schedule.fixed(1.seconds)) .ensuring { (printLine( "Finalizing or releasing a resource that is time-consuming" ) *> ZIO.sleep(7.seconds)).orDie } .fork _ <- fiber.interrupt.delay(4.seconds) _ <- printLine( "Starting another task when the interruption of the previous task finished" ) } yield () ``` In the above example, the release action takes some time for freeing up resources. So it slows down the call to `interrupt` the fiber. ### Fast Interruption As we saw in the previous section, the ZIO runtime gets stuck on interruption until the fiber's finalizer finishes its job. We can prevent this behavior by using `ZIO#disconnect` or `Fiber#interruptFork` which perform fiber's interruption in the background or in separate daemon fiber: Let's try the `Fiber#interruptFork`: ```scala for { fiber <- printLine("Working on the first job") .schedule(Schedule.fixed(1.seconds)) .ensuring { (printLine( "Finalizing or releasing a resource that is time-consuming" ) *> ZIO.sleep(7.seconds)).orDie } .fork _ <- fiber.interruptFork.delay(4.seconds) // fast interruption _ <- printLine( "Starting another task while interruption of the previous fiber happening in the background" ) } yield () ``` ### Interrupting Blocking Operations The `ZIO#attemptBlocking` is interruptible by default, but its interruption will not translate to JVM thread interruption. Instead, we can use `ZIO#attemptBlockingInterrupt` to translate the ZIO interruption of that effect into JVM thread interruption. For details and examples on interrupting blocking operations see [here](../core/zio/zio.md#blocking-synchronous-side-effects). ### Automatic Interruption If we never _cancel_ a running effect explicitly, ZIO performs **automatic interruption** for several reasons: 1. **Structured Concurrency** — If a parent fiber terminates, then by default, all child fibers are interrupted, and they cannot outlive their parent. We can prevent this behavior by using `ZIO#forkDaemon` or `ZIO#forkIn` instead of `ZIO#fork`. 2. **Parallelism** — If one effect fails during the execution of many effects in parallel, the others will be canceled. Examples include `foreachPar`, `zipPar`, and all other parallel operators. 3. **Timeouts** — If a running effect with a `timeout` has not been completed in the specified amount of time, then the execution is canceled. 4. **Racing** — The loser of a `race`, if still running, is canceled. ### Joining an Interrupted Fiber We can join an interrupted fiber, which will cause our fiber to become interrupted. This process preserves the proper finalization of the caller. So, **ZIO's concurrency model respects brackets even when we _join_ an interrupted fiber**: ```scala val myApp = ( for { fiber <- printLine("Running a job").delay(1.seconds).forever.fork _ <- fiber.interrupt.delay(3.seconds) _ <- fiber.join // Joining an interrupted fiber } yield () ).ensuring( printLine( "This finalizer will be executed without occurring any deadlock" ).orDie ) ``` A fiber that is interrupted because of joining another interrupted fiber will properly finalize; this is a distinction between ZIO and the other effect systems, which _deadlock_ the joining fiber. ## Thread Shifting - JVM By default, fibers give no guarantees as to which thread they execute on. They may shift between threads, especially as they execute for long periods of time. Fibers only ever shift onto the thread pool of the runtime system, which means that by default, fibers running for a sufficiently long time will always return to the runtime system's thread pool, even when their (asynchronous) resumptions were initiated from other threads. For performance reasons, fibers will attempt to execute on the same thread for a (configurable) minimum period, before yielding to other fibers. Fibers that resume from asynchronous callbacks will resume on the initiating thread, and continue for some time before yielding and resuming on the runtime thread pool. These defaults help to guarantee stack safety and cooperative multitasking. They can be changed in `Runtime` if automatic thread shifting is not desired. ## Types of Workloads Let's discuss the types of workloads that a fiber can handle. There are three types of them: 1. **CPU Work/Pure CPU Bound** is a workload that exploits the processing power of a CPU for pure computations of a huge chunk of work, e.g. complex numerical computations. 4. **Blocking I/O** is a workload that goes beyond pure computation by doing communication in a blocking fashion. For example, waiting for a certain amount of time to elapse or waiting for an external event to happen are blocking I/O operations. 5. **Asynchronous I/O** is a workload that goes beyond pure computation by doing communication asynchronously, e.g. registering a callback for a specific event. ### CPU Work What we refer to as CPU Work is pure computational firepower without involving any interaction and communication with the outside world. It doesn't involve any I/O operation. It's a pure computation. By I/O, we mean anything that involves reading from and writing to an external resource such as a file or a socket or web API, or anything that would be characterized as I/O. Fibers are designed to be **cooperative** which means that **they will yield to each other as required to preserve some level of fairness**. If we have a fiber that is doing CPU Work which passes through one or more ZIO operations such as `flatMap` or `map`, as long as there exists a touchpoint where the ZIO runtime system can sort of keep a tab on that ongoing CPU Work then that fiber will yield to other fibers. These touchpoints allow many fibers who are doing CPU Work to end up sharing the same thread. What if though, we have a CPU Work operation that takes a really long time to run? Let's say 30 seconds it does pure CPU Work very computationally intensive? What happens if we take that single gigantic function and put that into a `ZIO#attempt`? In that case there is no way for the ZIO Runtime to force that fiber to yield to other fibers. In this situation, the ZIO Runtime cannot preserve some level of fairness, and that single big CPU operation monopolizes the underlying thread. It is not a good practice to monopolize the underlying thread. ZIO has a special thread pool that can be used to do these computations. That's the **blocking thread pool**. The `ZIO#blocking` operator and its variants (see [here](../core/zio/zio.md#blocking-synchronous-side-effects)) can be used to run a big CPU Work on a dedicated thread. So, it doesn't interfere with all the other work that is going on simultaneously in the ZIO Runtime system. If a CPU Work doesn't yield quickly, then that is going to monopolize a thread. So how can we determine that our CPU Work can yield quickly or not? - If that overall CPU Work composes many ZIO operations, then due to the composition of ZIO operations, it has a chance to yield quickly to other fibers and doesn't monopolize a thread. - If that CPU work doesn't compose any ZIO operations, or we lift that from a legacy library, then the ZIO Runtime doesn't have any chance of yielding quickly to other fibers. So this fiber is going to monopolize the underlying thread. The best practice is to run those huge CPU Work on a dedicated thread pool, by lifting them with `ZIO#blocking`. :::note So as a rule of thumb, when we have a huge CPU Work that is not chunked with built-in ZIO operations, and thus going to monopolize the underlying thread, we should run that on a dedicated thread pool that is designed to perform CPU-driven tasks. ::: ### Blocking I/O Inside Java, there are many methods that will put our thread to sleep. For example, if we call `read` on a socket and there is nothing to read right now because not enough bytes have been read from the other side over the TCP/IP protocol, then that will put our thread to sleep. Most of the I/O operations and certainly all the classic I/O operations like `InputStream` and `OutputStream` are utilizing a locking mechanism that will `park` a thread. When we `write` to `OutputStream`, this method will `park` the thread and `wait` until the data has actually been written to file. It is the same way for `read` and similar blocking operations. Anytime we use a `lock`, anything in `java.util.concurrent.locks`, all those locks use this mechanism. All these operations are called blocking because they `park` the thread that is doing the work, and the thread that's doing the work goes to sleep. :::note What we refer to as blocking I/O is not necessarily just an I/O operation. Remember every time we use a `lock` we are also `park`ing a thread. It goes to `sleep`, and it has to be woken up again. We refer to this entire class of operations as **blocking I/O**. ::: There are multiple types of overhead associated with parking a thread: 1. When we `park` a thread then that thread is still consuming resources, it's still obviously consuming stack resources, the heap, and all metadata associated with the underlying thread in the JVM. 2. Since every JVM thread corresponds to an operating system level thread, there's a large amount of overhead even inside the operating system. Every thread has a pre-allocated stack size so that memory is reserved even if that thread is not doing any work. That memory is sort of reserved for the thread, and we cannot touch it. 3. Besides, the actual process of putting a thread to sleep and then later waking it up again is computationally intensive. It slows down our computations. This is why it has become a sort of best practice and part of the architectural pattern of reactive applications to design what is called **non-blocking application**. Non-blocking is synonymous with asynchronous. Non-blocking and asynchronous and to some degree even reactive, they're all trying to get at something which is what we want: scalable applications. Scalable applications cannot afford to have thousands of threads just sitting around doing nothing and just consuming work and taking a long time to wake up again. We cannot do blocking I/O in scalable applications. It is considered an anti-pattern because it is not efficient. That is not a way to build scalable applications, but nonetheless, we have to support that use case. Today, we have lots of common Java libraries that use blocking constructs, like `InputStream` and `OutputStream`, and `Reader` and `Writer`. Also, the JDBC is entirely blocking. The only way of doing database I/O in Java is blocking. So obviously, we have to do blocking I/O. We can do blocking I/O from a fiber. So is it best practice? No, it should be avoided whenever possible, but the reality is we have to do blocking I/O. Whenever we lift a blocking I/O operation into ZIO, the ZIO Runtime is executing a fiber that is doing blocking I/O. The underlying thread will be parked, and it has to be woken up later. It doesn't have any ability to avoid this. It can't stop an underlying thread from being parked. That's just the way these APIs are designed. So, we have to block. There's no way around that. That fiber will be monopolizing the underneath thread and therefore that thread is not available for performing all the work of the other fibers in the system. So that can be a bottleneck point in our application. And again, the solution to this problem is the same as the solution to the first class of problems, the CPU Work. The solution is to run this action **using the blocking thread pool** in ZIO which will ensure that this blocking code executes on its dedicated thread pool. **So it doesn't interfere or compete with the threads that are used for doing the bulk of work in your application**. So basically ZIO's philosophy is that if we _have_ to do CPU Work or blocking synchronous work that's ok we can do that. Just we need to do it in the right place. So it doesn't interfere with our primary thread pool. ZIO has one primary built-in fixed thread pool. This sort of workhorse thread pool is designed to be used for the majority of our application requirements. It has a certain number of threads in it and that stays constant over the lifetime of our application. Why is that the case? Well because for the majority of workloads in our applications, it does not actually help things to create more threads than the number of CPU cores. If we have eight cores, it does not accelerate any sort of processing to create more than eight threads. Because at the end of the day our hardware is only capable of running eight things at the same time. If we create a thousand threads on a system that can only run eight of them in parallel at a time, then what does the operating system do? As we have not enough CPU cores, the operating system starts giving a little slice of the eight cores to all these threads by switching between them over and over again. The overhead for context switching between threads is significant. The CPU has to load in new registers, refill all its caches, it has to go through all these crazy complex processes that interfere with its main job to get stuff done. There's significant overhead associated with that. As a result, it's not going to be very efficient. We are going to waste a lot of our time and resources just switching back and forth between all these threads, that would kill our application. So for that reason, ZIO's default thread pool is fixed with a number of threads equal to the number of CPU cores. That is the best practice. That means that no matter how much work we create if we create a hundred thousand fibers, they will still run on a fixed number of threads. Let's say we do blocking I/O on the main ZIO thread pool, so we have got eight threads all sitting and parked on a socket read. What happens to all the other 100000 fibers in our system? They line up in a queue waiting for their chance to run. That's not ideal. That's why we should take these effects that either do blocking I/O, or they do big CPU Work that's not chunked and run them using ZIO's blocking thread pool which will give us a dedicated thread. That dedicated thread is not efficient, but again, sometimes we have to interact with legacy code and legacy code is full of blocking code. We just need to be able to handle that gracefully and ZIO does that using the blocking thread pool. ### Asynchronous I/O The third category is asynchronous I/O, and we refer to it as Async Work. Async Work is code that whenever it runs into something that it needs to wait on, instead of blocking and parking the thread, it registers a callback, and returns immediately. It allows us to register a callback and when that result is available then our callback will be invoked. Callbacks are the fundamental way by which all async code on the JVM works. There is no mechanism in the JVM right now to support async code natively, but once that would happen in the future, probably in the Loom project, it will simplify a lot of things. But for now, in current days, sort of all published JVM versions have no such thing. The only way we can get a non-blocking async code is to have this callback registering mechanism. Callbacks have the pro that they don't wait for CPU. Instead of waiting to read the next chunk from a socket or instead of waiting to acquire a lock, all we have to do is call it and give it a callback. It doesn't need to do anything else. It can return control to the thread pool and then later on when that data has been read from the socket or when that lock has been acquired or when that amount of time has elapsed if we're sleeping for a certain amount of time, then our callback can be invoked. It has the potential to be extraordinarily efficient. The drawback of callbacks is they are not so pretty and fun to work with. They don't compose well with `try` / `finally` constructs. Error handling is really terrible, we have to do error propagation on our own. So that is what gave rise to data types like `Future` which eliminates the need for callbacks. By using `Future`s we can wrap callback-based APIs and get the benefits of async but without the callbacks. Also, it supports for-comprehension, so we can structure our code as a nice linear sequence. Similarly, in ZIO we never see a callback with ZIO, but fundamentally everything boils down to asynchronous operations on the JVM in a callback fashion. Callback base code is obscenely efficient, but it is extraordinarily painful to deal with directly. Data types like `Future` and `ZIO` allow us to avoid even seeing a callback in our code. With ZIO, we do not have to think about callbacks, unless sometimes, when we need to integrate with legacy code. ZIO has an appropriate constructor to turn that ugly callback-based API into a ZIO effect. It is the `async` constructor. Most of the ZIO operations that one would expect to be blocking do actually not block the underlying thread, but they offer blocking semantics managed by ZIO. For example, every time we see something like `ZIO.sleep` or when we take something from a queue (`queue.take`) or offer something to a queue (`queue.offer`) or if we acquire a permit from a semaphore (`semaphore.withPermit`) and so forth, we are just blocking semantically without actually blocking an underlying thread. If we use the corresponding methods in Java, like `Thread.sleep` or any of its `lock` machinery, then those methods are going to block a thread. So this is why we say that ZIO is 100% non-blocking, while Java threads are not. All of the pieces of machinery that ZIO gives us are 100% asynchronous and non-blocking. As they don't block and monopolize the thread, all of the async work is executed on the primary thread pool in ZIO. --- ## FiberId `FiberId` is the identity of a [Fiber](fiber.md), described by a globally unique sequence number and the time when it began life: * `id` — unique monotonically increasing sequence number `0,1,2,...`, derived from an atomic counter * `startTimeSeconds` — UTC time seconds, derived from `java.lang.System.currentTimeMillis / 1000` --- ## Fiber.Status `Fiber.Status` describes the current status of a [Fiber](fiber.md). Each fiber can be in one of the following status: - Done - Running - Suspended In the following example, we are going to `await` on a never-ending fiber and determine the id of that fiber, which we are blocking on: ```scala for { f1 <- ZIO.never.fork f2 <- f1.await.fork blockingOn <- f2.status .collect(()) { case Fiber.Status.Suspended(_, _, blockingOn) => blockingOn } .eventually } yield (assert(blockingOn == f1.id)) ``` --- ## Introduction to ZIO Fibers A Fiber can be thought of as a virtual thread. A Fiber is the analog of a Java thread (`java.lang.Thread`), but it performs much better. Fibers are implemented in such a fashion that a single JVM thread will execute many fibers. We can think of fibers as unbounded JVM threads. > **Warning** if you are not an experienced ZIO programmer: > > You should avoid using fibers manually. ZIO gives you many concurrent primitives like `raceWith`, `zipPar`, `foreachPar`, and so forth, which utilize fibers under the hood without any manual effort required. > > Fibers, just like threads, are low-level constructs. It's not generally recommended to deal with them in your code directly. It is very easy to make lots of mistakes or to introduce performance problems by manually using them. ## Why Fibers? There are some limitations with JVM threads: 1. **Threads are scarce** — Threads on the JVM map to the operating system level threads which imposes an upper bound on the number of threads that we can have inside our application. 2. **Expensive on creation** — The creation of threads is expensive in terms of time and memory complexity. 3. **Much Overhead on Context Switching** — Switching between the execution of one thread to another thread is not cheap, it takes a lot of time. 4. **Lack of Composability** — Threads are not typed. They don't have a meaningful return type. In Java, when we create a thread, we have to provide a `run` function that returns void. So threads cannot finish with any specific value. Due to this limitation, we cannot compose threads. Also, a thread has no type parameter for error. It is expected to throw any exception of type `Throwable` to signal errors. In the following sections, we are going to discuss the key features of fibers, and how fibers overcome the drawbacks of Java threads. ### Unbounded Size So whereas the mapping from JVM threads to operating system threads is one-to-one, **the mapping of fibers to threads is many-to-one**. Each JVM thread will end up executing anywhere from hundreds to thousands or even tens of thousands of fibers concurrently, by hopping back and forth between them as necessary. This gives us virtual threads that have the benefits of threads, but the scalability way beyond threads. In other words, fibers offer us massive concurrent **lightweight green threading** on the JVM. As a rough rule of thumb, we can have an application with a thousand real threads. No problem, modern servers can support applications with a thousand threads. However, we cannot have an application with a hundred thousand threads, that application will die. That just won't make any progress. The JVM nor our operating system can physically support a hundred thousand threads. However, it is no problem to have a Scala application with a hundred thousand fibers. Such an application can still perform in a very high-performance fashion, and the miracle that enables that to happen is fiber. ### Lightweight **JVM threads are expensive to create in terms of time and memory complexity.** Also it takes a lot of time to switch between one thread of execution to another. In contrast to that, fibers are virtual, and as they use **green threading**, they are considered to be **lightweight cooperative threads**. This means that fibers always _yield_ their executions to each other without the overhead of preemptive scheduling. ### Asynchronous Fibers are asynchronous, while threads are always synchronous. That is why fibers scale better than threads. ### Typed and Composable **Fibers have typed error and success values**. A fiber has two type parameters, `E` and `A`: - The `E` corresponds to the error channel. It indicates the error type with which the fiber can fail. - The `A` corresponds to the success value of the computation. That is the type with which the fiber can succeed. The fact that fibers are typed allows us to write more type-safe programs. Also, it increases the compositional properties of our programs because we can wait on a fiber to finish and then expect to receive a value of type `A`. ### Interrupt Safe Threads in Java can be terminated via the stop method, but this is not a safe operation. The stop operation has been [deprecated](https://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html). So this is not a safe way to force kill a thread. Instead, we should try to request an interruption of the thread, but in this case, **the thread may not respond to our request, and it may just go forever**. **Fiber has a safe version of this functionality that works very well**. Just like we can interrupt a thread, we can interrupt a fiber too, but interruption of fibers is much more reliable. It will always work, and **it probably works very fast**. We don't need to wait around, we can just try to interrupt them, and they will be gone very soon. ### Structured Concurrency With fibers, we can have hundreds of thousands and even millions of fibers that are started and working together. So we can reach a very massive concurrency with fibers. Now how can we manage all these fibers? Some of them are top-level fibers and some others are forked and become children of their parents. How can we manage their scopes, how to keep track of all fibers, and prevent them to leak? What happens to the execution of a child fiber if its parent execution is interrupted? The child fibers should be scoped to their parent fibers. We need a way to manage these scopes automatically. This is where structured concurrency shines. > _**Important**:_ > > It's worth mentioning that in the ZIO model, all code runs on fibers. There is no such thing as code that is executed outside of fibers. When we create a main function in ZIO that returns an effect, then even if we don't explicitly fork a fiber, the effect will be executed on what is called the main fiber. It's a top-level fiber. > >It's just like if we have a main function in Java then that main function will execute on the main thread. There is no code in Java that does not execute on a thread. All code executes on a thread even if you didn't create a thread. ZIO provides structured concurrency. The way ZIO's structured concurrency works is that **the child fibers are scoped to their parent fibers** which means that **when the parent effect finishes execution, then all childs' effects will be automatically interrupted**. So when we fork, and we get back a fiber, the fiber's lifetime is bound to the parent fiber that forked it. It is almost impossible to leak fibers because child fibers are guaranteed to complete before their parents. The structured concurrency gives us a way to reason about fiber lifespans. We can statically reason about the lifetimes of children fibers just by looking at our code. We don't need to insert complicated logic to keep track of all the child fibers and manually shut them down. #### Global Lifetime Sometimes we want a child fiber to outlive the scope of the parent. What can we do in that case? Well, ZIO offers an operator called `forkDaemon` which forks the fiber as a daemon fiber. Daemon fibers can outlive their parents. They can live forever. They run in the background doing their work until they end with failure or success. This gives us a way to spawn background jobs that should just keep on going regardless of what happens to the parent. #### Fine-grained Scope If we need a very flexible fine-grained control over the lifetime of a fiber there is another operator called `forkin`. We can fork a fiber inside a specific scope, and when that scope is closed then the fiber will be terminated. ## Fiber Data Types ZIO fiber contains a few data types that can help us to solve complex problems: - **[Fiber](fiber.md)** — A fiber value models an `IO` value that has started running, and is the moral equivalent of a green thread. - **[Fiber.Status](fiberstatus.md)** — `Fiber.Status` describes the current status of a Fiber. - **[FiberId](fiberid.md)** — `FiberId` describes the unique identity of a Fiber. --- ## Introduction ZIO contains a few data types that can help you solve complex problems in asynchronous and concurrent programming. ZIO data types categorize into these sections: 1. [Core Data Types](#core-data-types) 2. [Contextual Data Types](#contextual-data-types) 3. [State Management](#state-management) 4. [Concurrency](#concurrency) - [Fiber Primitives](#fiber-primitives) - [Concurrency Primitives](#concurrency-primitives) - [Synchronization Aids](#synchronization-aids) - [STM](#stm) 5. [Resource Management](#resource-management) 6. [Streaming](#streaming) 7. [Metrics](#metrics) 8. [Testing](#testing) 9. [Miscellaneous](#miscellaneous) ## Core Data Types - **[ZIO](core/zio/zio.md)** — `ZIO` is a value that models an effectful program, which might fail or succeed. + **[UIO](core/zio/uio.md)** — `UIO[A]` is a type alias for `ZIO[Any, Nothing, A]`. + **[URIO](core/zio/urio.md)** — `URIO[R, A]` is a type alias for `ZIO[R, Nothing, A]`. + **[Task](core/zio/task.md)** — `Task[A]` is a type alias for `ZIO[Any, Throwable, A]`. + **[RIO](core/zio/rio.md)** — `RIO[R, A]` is a type alias for `ZIO[R, Throwable, A]`. + **[IO](core/zio/io.md)** — `IO[E, A]` is a type alias for `ZIO[Any, E, A]`. - **[ZIOApp](core/zioapp.md)** — `ZIOApp` and the `ZIOAppDefault` are entry points for ZIO applications. - **[Runtime](core/runtime.md)** — `Runtime[R]` is capable of executing tasks within an environment `R`. - **[Exit](core/exit.md)** — `Exit[E, A]` describes the result of executing an `IO` value. - **[Cause](core/cause.md)** — `Cause[E]` is a description of a full story of a fiber failure. ## Contextual Data Types - **[ZEnvironment](contextual/zenvironment.md)** — `ZEnvironment[R]` is a built-in type-level map for the `ZIO` data type which is responsible for maintaining the environment of a `ZIO` effect. - **[ZLayer](contextual/zlayer.md)** — `ZLayer[-RIn, +E, +ROut]` is a recipe to build an environment of type `ROut`, starting from a value `RIn`, and possibly producing an error `E` during creation. + **[RLayer](contextual/rlayer.md)** — `RLayer[-RIn, +ROut]` is a type alias for `ZLayer[RIn, Throwable, ROut]`, which represents a layer that requires `RIn` as its input, it may fail with `Throwable` value, or returns `ROut` as its output. + **[ULayer](contextual/ulayer.md)** — `ULayer[+ROut]` is a type alias for `ZLayer[Any, Nothing, ROut]`, which represents a layer that doesn't require any services as its input, it can't fail, and returns `ROut` as its output. + **[Layer](contextual/layer.md)** — `Layer[+E, +ROut]` is a type alias for `ZLayer[Any, E, ROut]`, which represents a layer that doesn't require any services, it may fail with an error type of `E`, and returns `ROut` as its output. + **[URLayer](contextual/urlayer.md)** — `URLayer[-RIn, +ROut]` is a type alias for `ZLayer[RIn, Nothing, ROut]`, which represents a layer that requires `RIn` as its input, it can't fail, and returns `ROut` as its output. + **[TaskLayer](contextual/task-layer.md)** — `TaskLayer[+ROut]` is a type alias for `ZLayer[Any, Throwable, ROut]`, which represents a layer that doesn't require any services as its input, it may fail with `Throwable` value, and returns `ROut` as its output. ## State Management - **[ZState](state-management/zstate.md)**— It models a state that can be read from and written to during the execution of an effect. - **[Ref](state-management/global-shared-state.md)**— `Ref[A]` models a mutable reference to a value of type `A`. - **[FiberRef](state-management/fiberref.md)**— `FiberRef[A]` models a mutable reference to a value of type `A`. As opposed to `Ref[A]`, a value is bound to an executing `Fiber` only. You can think of it as Java's `ThreadLocal` on steroids. ## Concurrency ### Fiber Primitives - **[Fiber](fiber/fiber.md)** — A fiber value models an `IO` value that has started running, and is the moral equivalent of a green thread. - **[Fiber.Status](fiber/fiberstatus.md)** — `Fiber.Status` describe the current status of a Fiber. - **[FiberId](fiber/fiberid.md)** — `FiberId` describe the unique identity of a Fiber. ### Concurrency Primitives - **[Hub](concurrency/hub.md)** — A `Hub` is an asynchronous message hub that allows publishers to efficiently broadcast values to many subscribers. - **[Promise](concurrency/promise.md)** — A `Promise` is a model of a variable that may be set a single time, and awaited on by many fibers. - **[Semaphore](concurrency/semaphore.md)** — A `Semaphore` is an asynchronous (non-blocking) semaphore that plays well with ZIO's interruption. - **[Ref](concurrency/ref.md)** — `Ref[A]` models a mutable reference to a value of type `A`. The two basic operations are `set`, which fills the `Ref` with a new value, and `get`, which retrieves its current content. All operations on a `Ref` are atomic and thread-safe, providing a reliable foundation for synchronizing concurrent programs. - **[Ref.Synchronized](concurrency/refsynchronized.md)** — `Ref.Synchronized[A]` models a **mutable reference** to a value of type `A` in which we can store **immutable** data, and update it atomically **and** effectfully. - **[Queue](concurrency/queue.md)** — A `Queue` is an asynchronous queue that never blocks, which is safe for multiple concurrent producers and consumers. ### Synchronization Aids - **[ReentrantLock](sync/reentrantlock.md)**— The `ReentrantLock` is a synchronization tool that is useful for synchronizing blocks of code. - **[CountdownLatch](sync/countdownlatch.md)** — A synchronization aid that allows one or more fibers to wait until a set of operations being performed in other fibers completes. - **[CyclicBarrier](sync/cyclicbarrier.md)** — A synchronization aid that allows a set of fibers to all wait for each other to reach a common barrier point. - **[ConcurrentMap](sync/concurrentmap.md)** — A Map wrapper over `java.util.concurrent.ConcurrentHashMap` - **[ConcurrentSet](sync/concurrentset.md)** — A Set implementation over `java.util.concurrent.ConcurrentHashMap` ### STM - **[STM](stm/stm.md)** — An `STM` represents an effect that can be performed transactionally resulting in a failure or success. - **[TArray](stm/tarray.md)** — A `TArray` is an array of mutable references that can participate in transactions. - **[TSet](stm/tset.md)** — A `TSet` is a mutable set that can participate in transactions. - **[TMap](stm/tmap.md)** — A `TMap` is a mutable map that can participate in transactions. - **[TRef](stm/tref.md)** — A `TRef` is a mutable reference to an immutable value that can participate in transactions. - **[TPriorityQueue](stm/tpriorityqueue.md)** — A `TPriorityQueue` is a mutable priority queue that can participate in transactions. - **[TPromise](stm/tpromise.md)** — A `TPromise` is a mutable reference that can be set exactly once and can participate in transactions. - **[TQueue](stm/tqueue.md)** — A `TQueue` is a mutable queue that can participate in transactions. - **[TReentrantLock](stm/treentrantlock.md)** — A `TReentrantLock` is a reentrant read / write lock that can be composed. - **[TSemaphore](stm/tsemaphore.md)** — A `TSemaphore` is a semaphore that can participate in transactions. ## Resource Management - **[Scope](resource/scope.md)** — A scope in which resources can safely be used. - **[ZPool](resource/zpool.md)** — An asynchronous and concurrent generalized pool of reusable resources. ## Streaming - **[ZStream](stream/zstream/index.md)** — `ZStream` is a lazy, concurrent, asynchronous source of values. + **Stream** — `Stream[E, A]` is a type alias for `ZStream[Any, E, A]`, which represents a ZIO stream that does not require any services, and may fail with an `E`, or produce elements with an `A`. - **[ZSink](stream/zsink/index.md)** — `ZSink` is a consumer of values from a `ZStream`, which may produce a value when it has consumed enough. + **[Sink](stream/zsink/index.md)** — `Sink[InErr, A, OutErr, L, B]` is a type alias for `ZSink[Any, InErr, A, OutErr, L, B]`. - **[ZPipeline](stream/zpipeline.md)** — `ZPipeline` is a polymorphic stream transformer. - **[SubscriptionRef](stream/subscriptionref.md)** — `SubscriptionRef[A]` contains a current value of type `A` and a stream that can be consumed to observe all changes to that value. ## Metrics IO supports 5 types of Metrics: - **[Counter](observability/metrics/counter.md)** — The Counter is used for any value that increases over time like _request counts_. - **[Gauge](observability/metrics/gauge.md)** — The gauge is a single numerical value that can arbitrary goes up or down over time like _memory usage_. - **[Histogram](observability/metrics/histogram.md)** — The Histogram is used to track the distribution of a set of observed values across a set of buckets like _request latencies_. - **[Summary](observability/metrics/summary.md)** — The Summary represents a sliding window of a time series along with metrics for certain percentiles of the time series, referred to as quantiles like _request latencies_. - **[Frequency](observability/metrics/frequency.md)** — The Frequency is a metric that counts the number of occurrences of distinct string values. ## Testing - **[Spec](test/spec.md)**— A `Spec[R, E]` is the backbone of ZIO Test. All specs require an environment of type `R` and may potentially fail with an error of type `E`. - **[Assertion](test/assertions/index.md)**— An `Assertion[A]` is a test assertion that can be used to assert the predicate of type `A => Boolean`. - **[TestAspect](test/aspects/index.md)**— A `TestAspect` is an aspect that can be weaved into specs. We can think of an aspect as a polymorphic function, capable of transforming one test into another. - **[Gen](test/property-testing/built-in-generators.md)**— A `Gen[R, A]` represents a generator of values of type `A`, which requires an environment `R`. - **Test Service**— ZIO Test has the following out-of-the-box test services: - **[TestConsole](test/services/console.md)**— It allows testing of applications that interact with the console. - **[TestClock](test/services/clock.md)**— We can deterministically and efficiently test effects involving the passage of time without actually having to wait for the full amount of time to pass. - **[TestRandom](test/services/random.md)**— This service allows us having fully deterministic testing of code that deals with Randomness. - **[TestSystem](test/services/system.md)**— It supports deterministic testing of effects involving system properties. - **[Live](test/services/live.md)**— It provides access to the live environment from within the test environment for effects. - **[TestConfig](test/services/test-config.md)**— It provides access to default configuration settings used by ZIO Test. - **[Sized](test/services/sized.md)**— It enables _Sized Generators_ to access the size from the ZIO Test environment. ## Miscellaneous - **[Chunk](stream/chunk.md)**— `Chunk` is a fast, pure alternative to Arrays. - **[Supervisor](observability/supervisor.md)**— `Supervisor[A]` is allowed to supervise the launching and termination of fibers, producing some visible value of type `A` from the supervision. To learn more about these data types, please explore the pages above, or check out the Scaladoc documentation. --- ## Introduction to ZIO's Interruption Model While developing concurrent applications, there are several cases that we need to _interrupt_ the execution of other fibers, for example: 1. A parent fiber might start some child fibers to perform a task, and later the parent might decide that it doesn't need the result of some or all of the child fibers. 2. Two or more fibers start a race with each other. The fiber whose result is computed first wins and all other fibers are no longer needed so they should be interrupted. 3. In interactive applications, a user may want to stop some already running tasks, such as clicking on the "stop" button to prevent downloading more files. 4. Computations that run longer than expected should be aborted by using timeout operations. 5. When we have an application that perform compute-intensive tasks based on the user inputs, if the user changes the input we should cancel the current task and perform another one. ## Polling vs. Asynchronous Interruption A simple and naive way to implement fiber interruption is to provide a mechanism for one fiber to _kill/terminate_ another fiber. This is not a correct solution because if the target fiber is in the middle of changing a shared state it leads to an inconsistent state. So this solution doesn't guarantee to leave the shared mutable state internally consistent. Other than the very simple kill solution, there are two popular valid solutions to this problem: 1. **Semi-asynchronous Interruption (Polling for Interruption)**— Imperative languages such as Java often use polling to implement a semi-asynchronous signaling mechanism. In this model, a fiber sends a request for interruption of other fiber. The target fiber keeps polling the interrupt status, and based on the interrupt status will find out that whether there is an interruption request from other fibers. If so, it should terminate itself as soon as possible. Using this solution, the fiber itself takes care of critical sections. So while a fiber is in the middle of a critical section, if it receives an interruption request it should ignore the interruption and postpone the delivery of interruption during the critical section. The drawback of this solution is that, if the programmer forgets to poll regularly enough, then the target fiber becomes unresponsive and causes deadlocks. Another problem is that polling a global flag is not a functional operation and doesn't fit with ZIO's paradigm. 2. **Asynchronous Interruption**— In asynchronous interruption, a fiber is allowed to terminate another fiber. So the target fiber is not responsible for polling the status, instead in critical sections the target fiber disables the interruptibility of these regions. This is a purely-functional solution and doesn't require polling a global state. ZIO uses this solution for its interruption model. It is a fully asynchronous signalling mechanism. This mechanism doesn't have the drawback of forgetting to poll regularly and also it's fully compatible with the functional paradigm because in a purely-functional computation, we can abort the computation at any point, except for critical sections. ## When Does a Fiber Get Interrupted? There are several ways and situations that fibers can be interrupted. In this section we will introduce each one with an example of how to reproduce these situations: ### Calling `Fiber#interrupt` Operator A fiber can be interrupted by calling `Fiber#interrupt` on that fiber. Let's try to make a fiber and then interrupt it: ```scala object MainApp extends ZIOAppDefault { def task = { for { fn <- ZIO.fiberId.map(_.threadName) _ <- ZIO.debug(s"$fn starts a long running task") _ <- ZIO.sleep(1.minute) _ <- ZIO.debug("done!") } yield () } def run = for { f <- task.onInterrupt( ZIO.debug(s"Task interrupted while running") ).fork _ <- f.interrupt } yield () } ``` Here is the output of running this piece of code, which denotes that the task was interrupted: ``` Task interrupted while running ``` ### Interruption of Parallel Effects When composing multiple parallel effects, when one of them is interrupted the other fibers will be interrupted also. So if we have two parallel tasks, if one of them fails or gets interrupted, the other will be interrupted: ```scala object MainApp extends ZIOAppDefault { def debugInterruption(taskName: String) = (fibers: Set[FiberId]) => for { fn <- ZIO.fiberId.map(_.threadName) _ <- ZIO.debug( s"The $fn fiber which is the underlying fiber of the $taskName task " + s"interrupted by ${fibers.map(_.threadName).mkString(", ")}" ) } yield () def task[R, E, A](name: String)(zio: ZIO[R, E, A]): ZIO[R, E, A] = zio.onInterrupt(debugInterruption(name)) def debugMainFiber = for { fn <- ZIO.fiberId.map(_.threadName) _ <- ZIO.debug(s"Main fiber ($fn) starts executing the whole application.") } yield () def run = { // self interrupting fiber val first = task("first")(ZIO.interrupt) // never ending fiber val second = task("second")(ZIO.never) debugMainFiber *> { // uncomment each line and run the code to see the result // first fiber will be interrupted first *> second // never ending application // second *> first // first fiber will be interrupted // first <*> second // never ending application // second <*> first // first and second will be interrupted // first <&> second // first and second will be interrupted // second <&> first } } } ``` In the above code the `first <&> second` is a parallel composition of the `first` and `second` tasks. When we run them together, the `zipWithPar`/`<&>` operator will run these two tasks in two parallel fibers. If either side of this operator fails or is interrupted the other side will be interrupted. ### Child Fibers Are Scoped to Their Parents 1. If a child fiber does not complete its job or does not join its parent before the parent has completed its job, the child fiber will be interrupted: ```scala object MainApp extends ZIOAppDefault { def debugInterruption(taskName: String) = (fibers: Set[FiberId]) => for { fn <- ZIO.fiberId.map(_.threadName) _ <- ZIO.debug( s"the $fn fiber which is the underlying fiber of the $taskName task " + s"interrupted by ${fibers.map(_.threadName).mkString(", ")}" ) } yield () def run = for { fn <- ZIO.fiberId.map(_.threadName) _ <- ZIO.debug(s"$fn starts working.") child = for { cfn <- ZIO.fiberId.map(_.threadName) _ <- ZIO.debug(s"$cfn starts working by forking from its parent ($fn)") _ <- ZIO.never } yield () _ <- child.onInterrupt(debugInterruption("child")).fork _ <- ZIO.sleep(1.second) _ <- ZIO.debug(s"$fn finishes its job and is going go exit.") } yield () } ``` Here is the result of one of the executions of this sample code: ``` zio-fiber-2 starts working. zio-fiber-7 starts working by forking from its parent (zio-fiber-2) zio-fiber-2 finishes its job and is going to exit. the zio-fiber-7 fiber which is the underlying fiber of the child task interrupted by zio-fiber-2 ``` 2. If a parent fiber is interrupted, all its children will be interrupted: ```scala object MainApp extends ZIOAppDefault { def debugInterruption(taskName: String) = (fibers: Set[FiberId]) => for { fn <- ZIO.fiberId.map(_.threadName) _ <- ZIO.debug( s"The $fn fiber which is the underlying fiber of the $taskName task " + s"interrupted by ${fibers.map(_.threadName).mkString(", ")}" ) } yield () def task = for { fn <- ZIO.fiberId.map(_.threadName) _ <- ZIO.debug(s"$fn starts running that will print random numbers and booleans") f1 <- Random.nextIntBounded(100) .debug("random number ") .schedule(Schedule.spaced(1.second).forever) .onInterrupt(debugInterruption("random number")) .fork f2 <- Random.nextBoolean .debug("random boolean ") .schedule(Schedule.spaced(2.second).forever) .onInterrupt(debugInterruption("random boolean")) .fork _ <- f1.join _ <- f2.join } yield () def run = for { f <- task.fork _ <- ZIO.sleep(5.second) _ <- f.interrupt } yield () } ``` Here is one sample output for this program: ``` zio-fiber-7 starts running that will print random numbers and booleans random number : 65 random boolean : true random number : 51 random number : 46 random boolean : true random number : 30 The zio-fiber-9 fiber which is the underlying fiber of the random boolean task interrupted by zio-fiber-7 The zio-fiber-8 fiber which is the underlying fiber of the random number task interrupted by zio-fiber-7 ``` ## Blocking Operations ### Interruption of Blocking Operations By default, when we convert a blocking operation into a ZIO effect using `attemptBlocking`, there is no guarantee that if that effect is interrupted the underlying effect will be interrupted. Let's create a blocking effect from an endless loop: ```scala for { _ <- Console.printLine("Starting a blocking operation") fiber <- ZIO.attemptBlocking { while (true) { Thread.sleep(1000) println("Doing some blocking operation") } }.ensuring( Console.printLine("End of a blocking operation").orDie ).fork _ <- fiber.interrupt.schedule( Schedule.delayed( Schedule.duration(1.seconds) ) ) } yield () ``` When we interrupt this loop after one second it will still not stop. It will only stop when the entire JVM stops. The `attemptBlocking` doesn't translate the ZIO interruption into thread interruption (`Thread.interrupt`). Instead, we should use `attemptBlockingInterrupt` to create interruptible blocking effects: ```scala for { _ <- Console.printLine("Starting a blocking operation") fiber <- ZIO.attemptBlockingInterrupt { while(true) { Thread.sleep(1000) println("Doing some blocking operation") } }.ensuring( Console.printLine("End of the blocking operation").orDie ).fork _ <- fiber.interrupt.schedule( Schedule.delayed( Schedule.duration(3.seconds) ) ) } yield () ``` Notes: 1. If we are converting a blocking I/O to a ZIO effect, it would be better to use `attemptBlockingIO` which refines the error type to `java.io.IOException`. 2. The `attemptBlockingInterrupt` method adds significant overhead. So for performance-sensitive applications, it is better to handle interruptions manually using `attemptBlockingCancelable`. ### Cancellation of Blocking Operation Some blocking operations do not respect `Thread#interrupt` by swallowing `InterruptedException`. So they will not be interrupted via `attemptBlockingInterrupt`. Instead, they may provide us an API to signal them to _cancel_ their operation. The following `BlockingService` will not be interrupted in case of a `Thread#interrupt` call, but it checks the `released` flag constantly. If this flag becomes true, the blocking service will finish its job: ```scala final case class BlockingService() { private val released = new AtomicReference(false) def start(): Unit = { while (!released.get()) { println("Doing some blocking operation") try Thread.sleep(1000) catch { case _: InterruptedException => () // Swallowing InterruptedException } } println("Blocking operation closed.") } def close(): Unit = { println("Releasing resources and ready to be closed.") released.getAndSet(true) } } ``` So to translate ZIO interruption into cancellation of these types of blocking operations we should use `attemptBlockingCancelable`. This method takes a `cancel` effect which is responsible for signalling the blocking code to close itself when ZIO interruption occurs: ```scala val myApp = for { service <- ZIO.attempt(BlockingService()) fiber <- ZIO.attemptBlockingCancelable( effect = service.start() )( cancel = ZIO.succeed(service.close()) ).fork _ <- fiber.interrupt.schedule( Schedule.delayed( Schedule.duration(3.seconds) ) ) } yield () ``` Here is another example of the cancellation of a blocking operation. When we `accept` a server socket, this blocking operation will never be interrupted until we close it using the `ServerSocket#close` method: ```scala def accept(ss: ServerSocket): Task[Socket] = ZIO.attemptBlockingCancelable(ss.accept())(ZIO.succeed(ss.close())) ``` ## Disabling Interruption of Fibers As we discussed earlier, it is dangerous for fibers to interrupt others. The danger with such an interruption is that: - If the interruption occurs during the execution of an operation that must be _finalized_, the finalization will not be executed. - If this interruption occurs in the middle of a _critical section_, it will cause an application state to become inconsistent. - It is also a threat to _resource safety_. If the fiber is in the middle of acquiring a resource and is interrupted, the application will leak resources. ZIO introduces the `uninterruptible` and `uninterruptibleMask` operations for this purpose. The former creates a region of code uninterruptible and the latter has the same functionality but gives us a `restore` function that can be applied to any region of code to restore the interruptibility of that region. These operators are advanced and very low-level so we don't use them in regularly in application development unless we know what we are doing as library designers. If you find yourself using these operators, think again about refactoring your code using high-level operators like `ZIO#onInterrupt`, `ZIO#onDone`, `ZIO#ensuring`, `ZIO.acquireRelease*` and many other concurrent operators like `race`, `foreachPar`, etc. --- ## Introduction to Logging in ZIO ZIO supports a lightweight built-in logging facade that standardizes the interface to logging functionality. So it doesn't replace existing logging libraries, but also we can plug it into one of the existing logging backends. We can easily log using the `ZIO.log` function: ```scala val app = for { _ <- ZIO.log("Application started!") name <- Console.readLine("Please enter your name: ") _ <- ZIO.log("User entered its name: $name") _ <- Console.printLine("Hello, $name") } yield () ``` ## Logging Levels To log with a specific log-level, we can use the `ZIO.logLevel` combinator: ```scala ZIO.logLevel(LogLevel.Warning) { ZIO.log("The response time exceeded its threshold!") } ``` Or we can use the following functions directly: * `ZIO.logDebug` * `ZIO.logError` * `ZIO.logFatal` * `ZIO.logInfo` * `ZIO.logWarning` For example, for log with the error level, we can use `ZIO.logError` like this: ```scala ZIO.logError("File does not exist: ~/var/www/favicon.ico") ``` ## Spans It also supports spans: ```scala ZIO.logSpan("myspan") { ZIO.sleep(1.second) *> ZIO.log("The job is finished!") } ``` ZIO Logging calculates the running duration of that span and includes that in the logging data corresponding to its span label. ## Log Annotations ZIO by default adds some contextual information to the log messages, like the timestamp, log level, fiber ID, and source location. Sometimes these default contextual information are not sufficient to understand the circumstances under which they were generated. In such cases, we need to add custom contextual information to the log messages. We can do this using log annotations. ### ZIO's Built-in Log Annotation For example, in microservice environments, we might have several services that are communicating with each other. In such cases, we might want to correlate the logs generated by different services. We can do this by adding a log annotation called `correlation_id`. This log annotation can be very simple, just a string, that is passed along with requests and responses. So, when we log a message, we know which request or response it is related to. ZIO has a built-in log annotation API that allows us to add such custom contextual information to the log messages: ```scala object MainApp extends ZIOAppDefault { def randomDelay = Random.nextIntBounded(1000).flatMap(t => ZIO.sleep(t.millis)) def run = ZIO.foreachParDiscard(Chunk("UserA", "UserB", "UserC")) { user => ZIO.logAnnotate("correlation_id", user) { for { _ <- ZIO.log("fetching user from database") *> randomDelay _ <- ZIO.log("downloading user's profile picture") *> randomDelay } yield () } } } ``` Here is an example of the log messages generated by the above code, each log message contains the `correlation_id` log annotation: ``` timestamp=2024-05-14T15:44:50.734129Z level=INFO thread=#zio-fiber-851563977 message="fetching user from database" location=zio.examples.MainApp.run file=MainApp.scala line=12 correlation_id=UserC timestamp=2024-05-14T15:44:50.734127Z level=INFO thread=#zio-fiber-41969365 message="fetching user from database" location=zio.examples.MainApp.run file=MainApp.scala line=12 correlation_id=UserA timestamp=2024-05-14T15:44:50.734123Z level=INFO thread=#zio-fiber-1775966732 message="fetching user from database" location=zio.examples.MainApp.run file=MainApp.scala line=12 correlation_id=UserB timestamp=2024-05-14T15:44:50.928248Z level=INFO thread=#zio-fiber-851563977 message="downloading user's profile picture" location=zio.examples.MainApp.run file=MainApp.scala line=13 correlation_id=UserC timestamp=2024-05-14T15:44:51.054287Z level=INFO thread=#zio-fiber-41969365 message="downloading user's profile picture" location=zio.examples.MainApp.run file=MainApp.scala line=13 correlation_id=UserA timestamp=2024-05-14T15:44:51.534263Z level=INFO thread=#zio-fiber-1775966732 message="downloading user's profile picture" location=zio.examples.MainApp.run file=MainApp.scala line=13 correlation_id=UserB ``` ### Typed Log Annotations In more complex scenarios, we might want to add more structured information to the log messages. For example, we might want to add the user information to the log messages. In such cases, we need a typed log annotation that supports structured information, e.g. a `User` case class that contains the user's id, name, email, etc. Using [ZIO Logging](https://zio.dev/zio-logging), we can define typed log annotations using the `LogAnnotation` class. So let's add required dependencies to the `build.sbt` file: ```scala libraryDependencies ++= Seq( "dev.zio" %% "zio-logging" % "4.0.2", "dev.zio" %% "zio-json" % "0.7.38" ) ``` Now, let's assume we have a `User` case class: ```scala case class User(firstName: String, lastName: String) ``` We can define a typed log annotation for the `User` case class like this: ```scala object TypedLogAnnotationExample extends ZIOAppDefault { case class User(firstName: String, lastName: String) object User { implicit val encoder = DeriveJsonEncoder.gen[User] } private val userLogAnnotation = LogAnnotation[User]("user", (_, u) => u, _.toJson) private val logConfig = ConsoleLoggerConfig.default.copy( format = LogFormat.default + LogFormat.annotation(LogAnnotation.TraceId) + LogFormat.annotation(userLogAnnotation) ) override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = Runtime.removeDefaultLoggers >>> consoleJsonLogger(logConfig) def run = for { _ <- ZIO.foreachPar(List(User("John", "Doe"), User("Jane", "Doe"))) { user => { ZIO.logInfo("Starting operation") *> ZIO.sleep(500.millis) *> ZIO.logInfo("Stopping operation") } @@ userLogAnnotation(user) } _ <- ZIO.logInfo("Done") } yield () } ``` The log messages generated by the above code will contain the `user` log annotation: ``` {"timestamp":"2024-05-14T20:37:33.744171+04:30","level":"INFO","thread":"zio-fiber-6","message":"Starting operation","user":{"firstName":"Jane","lastName":"Doe"}} {"timestamp":"2024-05-14T20:37:33.744166+04:30","level":"INFO","thread":"zio-fiber-5","message":"Starting operation","user":{"firstName":"John","lastName":"Doe"}} {"timestamp":"2024-05-14T20:37:34.334837+04:30","level":"INFO","thread":"zio-fiber-5","message":"Stopping operation","user":{"firstName":"John","lastName":"Doe"}} {"timestamp":"2024-05-14T20:37:34.334848+04:30","level":"INFO","thread":"zio-fiber-6","message":"Stopping operation","user":{"firstName":"Jane","lastName":"Doe"}} {"timestamp":"2024-05-14T20:37:34.337953+04:30","level":"INFO","thread":"zio-fiber-4","message":"Done"} ``` ## Further Reading * [ZIO Logging](https://zio.dev/zio-logging) * [How to Enable Logging in a ZIO Application](../../guides/tutorials/enable-logging-in-a-zio-application.md) * [How to Create a Custom Logger for a ZIO Application?](../../guides/tutorials/create-custom-logger-for-a-zio-application.md) --- ## Counter A `Counter` is a metric representing a single numerical value that may be incremented over time. A typical use of this metric would be to track the number of a certain type of request received. With a counter, the quantity of interest is the cumulative value over time, as opposed to a [gauge](gauge.md) where the quantity of interest is the value as of a specific point in time. ## API With one of the following constructors, we can create a counter of `Long`, `Double` or `Int` type: ```scala object Metric { def counter(name: String): Counter[Long] = ??? def counterDouble(name: String): Counter[Double] = ??? def counterInt(name: String): Counter[Int] = ??? } ``` ## Use Cases We use the counter metric type for any value that increases, such as request counts. Note that we should never use the counter for a value that can decrease. So when we should use counters? - When we want to track a value over time, that only goes up - When we want to measure the increasing rate of something, how fast something is growing, such as request rates. Here are some of the use cases: - Request Counts - Completed Tasks - Error Counts ## Examples Create a counter named `countAll` which is incremented by `1` every time it is invoked: ```scala val countAll = Metric.counter("countAll").fromConst(1) ``` Now the counter can be applied to any effect. Note, that the same aspect can be applied to more than one effect. In the example we would count the sum of executions of both effects in the for comprehension: ```scala val myApp = for { _ <- ZIO.unit @@ countAll _ <- ZIO.unit @@ countAll } yield () ``` Or we can apply them in recurrence situations: ```scala (zio.Random.nextLongBounded(10) @@ Metric.counter("request_counts")).repeatUntil(_ == 7) ``` Create a counter named `countBytes` that can be applied to effects having the output type `Double`: ```scala val countBytes = Metric.counter("countBytes") ``` Now we can apply it to effects producing `Double` (in a real application the value might be the number of bytes read from a stream or something similar): ```scala val myApp = Random.nextLongBetween(0, 100) @@ countBytes ``` --- ## Frequency A `Frequency` represents the number of occurrences of specified values. We can think of a `Frequency` as a set of counters associated with each value except that new counters will automatically be created when new values are observed. Essentially, a `Frequency` is a set of related counters sharing the same name and tags. The counters are set apart from each other by an additional configurable tag. The values of the tag represent the observed distinct values. ## API ```scala object Metric { def frequency(name: String): Frequency[String] = ??? } ``` ## Use Cases Sets are used to count the occurrences of distinct string values: - Tracking number of invocations for each service, for an application that uses logical names for its services. - Tracking frequency of different types of failures. ## Examples Create a `Frequency` to observe the occurrences of unique `Strings`. It can be applied to effects yielding a `String`: ```scala val freq = Metric.frequency("MySet") ``` Now we can generate some keys within an effect and start counting the occurrences for each value: ```scala (Random.nextIntBounded(10).map(v => s"MyKey-$v") @@ freq).repeatN(100) ``` --- ## Gauge A `Gauge` is a metric representing a single numerical value that may be _set_ or _adjusted_. A typical use of this metric would be to track the current memory usage. With a gauge, the quantity of interest is the current value, as opposed to a counter where the quantity of interest is the cumulative values over time. A gauge is a named variable of type _Double_ that can change over time. It can either be set to an absolute value or relative to the current value. ## API ```scala object Metric { def gauge(name: String): Gauge[Double] = ??? } ``` ## Use Case The gauge metric type is the best choice for things that their values can go down as well as up, such as queue size, and we don't want to query their rates. Thus, they are used to measuring things that have a particular value at a certain point in time: - Memory Usage - Queue Size - In-Progress Request Counts - Temperature ## Examples Create a gauge that can be set to absolute values, it can be applied to effects yielding a `Double`: ```scala val absoluteGauge = Metric.gauge("setGauge") ``` Now we can apply these gauges to effects having an output type `Double`. Note that we can instrument an effect with any number of aspects if the type constraints are satisfied: ```scala for { _ <- Random.nextDoubleBetween(0.0d, 100.0d) @@ absoluteGauge @@ countAll } yield () ``` --- ## Histogram A `Histogram` is a metric representing a collection of numerical with the distribution of the cumulative values over time. They organize a range of measurements into distinct intervals, known as buckets, and record the frequency of measurements falling within each bucket. Histograms allow representing not only the value of the quantity being measured but its distribution. They are representation of the distribution of a dataset, which organizes the data into buckets and display the frequency or count of data points within each bucket. ## Internals In a histogram, we assign the incoming samples to pre-defined buckets. So each data point increases the count for the bucket that it falls into, and then the individual samples are discarded. As histograms are bucketed, we can aggregate data across multiple instances. Histograms are a typical way to measure percentiles. We can look at bucket counts to estimate a specific percentile. A histogram observes _Double_ values and counts the observed values in buckets. Each bucket is defined by an upper boundary, and the count for a bucket with the upper boundary `b` increases by `1` if an observed value `v` is less or equal to `b`. As a consequence, all buckets that have a boundary `b1` with `b1 > b` will increase by `1` after observing `v`. A histogram also keeps track of the overall count of observed values, and the sum of all observed values. By definition, the last bucket is always defined as `Double.MaxValue`, so that the count of observed values in the last bucket is always equal to the overall count of observed values within the histogram. The mental model for histogram is inspired from [Prometheus](https://prometheus.io/docs/concepts/metric_types/#histogram). ## API ```scala object Metric { def histogram( name: String, boundaries: Histogram.Boundaries ): Histogram[Double] = ??? def timer( name: String, description: String, chronoUnit: ChronoUnit ): Metric[MetricKeyType.Histogram, Duration, MetricState.Histogram] = ??? def timer( name: String, chronoUnit: ChronoUnit, boundaries: Chunk[Double] ): Metric[MetricKeyType.Histogram, Duration, MetricState.Histogram] = ??? } ``` ## Use Cases Histograms are widely used in software metrics for various purposes. They are useful in analyzing the performance of software systems. They can represent metrics such as **response times**, **latencies**, or **throughput**. By visualizing the distribution of these metrics in a histogram, developers can identify **performance bottlenecks**, **outliers**, or **variations**. This information aids in optimizing code, infrastructure, and system configurations to improve overall performance. Histogram measures the frequency of value observations that fall into specific _pre-defined buckets_. For example, we can measure the request duration of an HTTP request using histograms. Rather than storing every duration for every request, the histogram will make an approximation by storing the frequency of requests that fall into pre-defined particular buckets. Thus, histograms are the best choice in these situations: - When we want to observe many values and then later want to calculate the percentile of observed values - When we can estimate the range of values upfront, as the histogram put the observations into pre-defined buckets - When accuracy is not so important, and we don't want the exact values because of the lossy nature of bucketing data in histograms - When we need to aggregate histograms across multiple instances ## Examples ### Histogram With Linear Buckets Create a histogram with 12 buckets: `0..100` in steps of `10` and `Double.MaxValue`. It can be applied to effects yielding a `Double`: ```scala val histogram = Metric.histogram("histogram", MetricKeyType.Histogram.Boundaries.linear(0, 10, 11)) ``` Now we can apply the histogram to effects producing `Double`: ```scala Random.nextDoubleBetween(0.0d, 120.0d) @@ histogram ``` ### Timer Metric Here is an example of adding timer metric to track workflow durations: ```scala object Example extends ZIOAppDefault { def workflow = ZIO.succeed(42) def randomDelay = for { i <- Random.nextLongBetween(1L, 10) _ <- ZIO.sleep(Duration.fromMillis(i)) } yield () val timer = Metric.timer( name = "timer", chronoUnit = ChronoUnit.MILLIS, boundaries = Chunk.iterate(1.0, 10)(_ + 1.0) ) val run = ((workflow <* randomDelay) @@ timer.trackDuration).repeatN(99) } ``` If we add prometheus layer, we expose the metrics which is something like this: ```csv # TYPE timer histogram # HELP timer timer_bucket{time_unit="millis",le="1.0",} 6.0 1686581577320 timer_bucket{time_unit="millis",le="2.0",} 16.0 1686581577320 timer_bucket{time_unit="millis",le="3.0",} 27.0 1686581577320 timer_bucket{time_unit="millis",le="4.0",} 41.0 1686581577320 timer_bucket{time_unit="millis",le="5.0",} 49.0 1686581577320 timer_bucket{time_unit="millis",le="6.0",} 60.0 1686581577320 timer_bucket{time_unit="millis",le="7.0",} 70.0 1686581577320 timer_bucket{time_unit="millis",le="8.0",} 85.0 1686581577320 timer_bucket{time_unit="millis",le="9.0",} 99.0 1686581577320 timer_bucket{time_unit="millis",le="10.0",} 99.0 1686581577320 timer_bucket{time_unit="millis",le="+Inf",} 100.0 1686581577320 timer_sum{time_unit="millis",} 603.0 1686581577320 timer_count{time_unit="millis",} 100.0 1686581577320 timer_min{time_unit="millis",} 1.0 1686581577320 timer_max{time_unit="millis",} 66.0 1686581577320⏎ ``` This Prometheus result represents a histogram metric called "timer" with a time unit of milliseconds. The histogram provides information about the distribution of workflow durations. The histogram is divided into multiple buckets, each representing a range of workflow durations. The "le" label indicates the upper bound of each bucket. The values next to each bucket indicate the count or frequency of measurements falling within that bucket. For instance, `"timer_bucket{time_unit="millis",le="5.0",} 49.0"` means there are 49 measurements with duration less than or equal to 5.0 millisecond. --- ## Introduction to ZIO Metrics In highly concurrent applications, things are interconnected, so maintaining such a setup to run smoothly and without application downtimes is very challenging. Imagine we have a complex infrastructure with lots of services. Services are replicated and distributed across our servers. So we have no insight into what happening across our services at the level of errors, response latency, service uptime, etc. With ZIO Metrics, we can capture these different metrics and provide them to a _metric service_ for later investigation. ZIO supports 5 types of Metrics: * **[Counter](counter.md)** — The Counter is used for any value that increases over time like _request counts_. * **[Gauge](gauge.md)** — The gauge is a single numerical value that can go up or down over time like _memory usage_. * **[Histogram](histogram.md)** — The Histogram is used to track the distribution of a set of observed values across a set of buckets like _request latencies_. * **[Summary](summary.md)** — The Summary represents a sliding window of a time series along with metrics for certain percentiles of the time series, referred to as quantiles like _request latencies_. * **[Frequency](frequency.md)** — The Frequency is a metric that counts the number of occurrences of distinct string values. All ZIO Metrics are defined in the form of ZIO Aspects that can be applied to effects without changing the signature of the effect it is applied to: ```scala def memoryUsage: ZIO[Any, Nothing, Double] = { ZIO .succeed(getRuntime.totalMemory() - getRuntime.freeMemory()) .map(_ / (1024.0 * 1024.0)) @@ Metric.gauge("memory_usage") } ``` After adding metrics into our application, whenever we want we can capture snapshots of all metrics recorded by our application, by any of the metric backends supported by [ZIO Metrics Connectors](https://github.com/zio/zio-metrics-connectors) project. Here is an example of adding a Prometheus connector to our application: ```scala title="examples/jvm/src/main/scala/zio/examples/metrics/MetricAppExample.scala" package zio.examples.metrics object MetricAppExample extends ZIOAppDefault { def memoryUsage: ZIO[Any, Nothing, Double] = { ZIO .succeed(getRuntime.totalMemory() - getRuntime.freeMemory()) .map(_ / (1024.0 * 1024.0)) @@ Metric.gauge("memory_usage") } private val httpApp = Routes( Method.GET / "metrics" -> handler(ZIO.serviceWithZIO[PrometheusPublisher](_.get.map(Response.text))), Method.GET / "foo" -> handler { for { _ <- memoryUsage time <- Clock.currentDateTime } yield Response.text(s"$time\t/foo API called") } ) override def run = Server .serve(httpApp) .provide( // ZIO Http default server layer, default port: 8080 Server.default, // The prometheus reporting layer prometheus.prometheusLayer, prometheus.publisherLayer, // Interval for polling metrics ZLayer.succeed(MetricsConfig(5.seconds)) ) } ``` To run this example we need to add following lines to our `build.sbt` file: ```scala ``` ZIO Metrics Connectors currently supports the following backends: - [Prometheus](https://prometheus.io/) - [Datadog](https://www.datadoghq.com/) - [New Relic](https://newrelic.com/) - [StatsD](https://github.com/statsd/statsd) --- ## JVM Metrics ZIO has built-in support for collecting JVM Metrics. These metrics are a direct port of the JVM metrics provided by the [Prometheus Java Hotspot library](https://github.com/prometheus/client_java/tree/master/simpleclient_hotspot) and compatible with that library. There are five categories of JVM metrics. Let's look at them one by one: - Buffer Pools - `jvm_buffer_pool_used_bytes` — Used bytes of a given JVM buffer pool. - `jvm_buffer_pool_capacity_bytes` — Bytes capacity of a given JVM buffer pool. - `jvm_buffer_pool_used_buffers` — Used buffers of a given JVM buffer pool. - Class Loading - `jvm_classes_loaded` — The number of classes that are currently loaded in the JVM - `jvm_classes_loaded_total` — The total number of classes that have been loaded since the JVM has started execution - `jvm_classes_unloaded_total` — The total number of classes that have been unloaded since the JVM has started execution - Garbage Collector - `jvm_gc_collection_seconds_sum` — Time spent in a given JVM garbage collector in seconds. - `jvm_gc_collection_seconds_count` - Memory Allocation - `jvm_memory_pool_allocated_bytes_total` — Total bytes allocated in a given JVM memory pool. Only updated after GC, not continuously. - Memory Pools - `jvm_memory_bytes_used` — Used bytes of a given JVM memory area. - `jvm_memory_bytes_committed` — Committed (bytes) of a given JVM memory area. - `jvm_memory_bytes_max` — Max (bytes) of a given JVM memory area. - `jvm_memory_bytes_init` — Initial bytes of a given JVM memory area. - `jvm_memory_pool_bytes_used` — Used bytes of a given JVM memory pool. - `jvm_memory_pool_bytes_committed` — Committed bytes of a given JVM memory pool. - `jvm_memory_pool_bytes_max` — Max bytes of a given JVM memory pool. - `jvm_memory_pool_bytes_init` — Initial bytes of a given JVM memory pool. - Standard - `process_cpu_seconds_total` — Total user and system CPU time spent in seconds. - `process_start_time_seconds` — Start time of the process since unix epoch in seconds. - `process_open_fds` — Number of open file descriptors. - `process_max_fds` — Maximum number of open file descriptors. - `process_virtual_memory_bytes` — Virtual memory size in bytes. - `process_resident_memory_bytes` — Resident memory size in bytes. - Thread - `jvm_threads_current` — Current thread count of a JVM. - `jvm_threads_daemon` — Daemon thread count of a JVM. - `jvm_threads_peak` — Peak thread count of a JVM. - `jvm_threads_started_total` — Started thread count of a JVM. - `jvm_threads_deadlocked` — Cycles of JVM-threads that are in deadlock waiting to acquire object monitors or ownable synchronizers. - `jvm_threads_deadlocked_monitor` — Cycles of JVM-threads that are in deadlock waiting to acquire object monitors. - `jvm_threads_state` — Current count of threads by state. - Version Info - `jvm_info` - `version` — java.runtime.version - `vendor` — java.vm.vendor - `runtime` — java.runtime.name ## Collecting Metrics ### Collecting Inside a ZIO Application JVM Metrics are collection of the following ZIO services: - BufferPools - ClassLoading - GarbageCollector - MemoryAllocation - MemoryPools - Standard - Thread - VersionInfo All of these services are available in the `zio.metrics.jvm` package. Each service has a `live` implementation that can be used to collect metrics, or we can use all of them at once with by providing `DefaultJvmMetrics.live` layer to our application. ### Collecting as a Sidecar to a ZIO Application ZIO JVM metrics have built-in applications that collect the JVM metrics. They can be composed with other ZIO applications as a _sidecar_. By doing so, we are able to collect JVM metrics without modifying our main ZIO application. They will be executed as a daemon alongside the main app: ```scala title="examples/jvm/src/main/scala/zio/examples/metrics/JvmMetricAppExample.scala" package zio.examples.metrics object JvmMetricAppExample extends ZIOAppDefault { private val httpApp = Routes( Method.GET / "metrics" -> handler(ZIO.serviceWithZIO[PrometheusPublisher](_.get.map(Response.text))) ) override def run = Server .serve(httpApp) .provide( // ZIO Http default server layer, default port: 8080 Server.default, // The prometheus reporting layer prometheus.prometheusLayer, prometheus.publisherLayer, // Interval for polling metrics ZLayer.succeed(MetricsConfig(5.seconds)), // Default JVM Metrics DefaultJvmMetrics.liveV2.unit ) } ``` --- ## MetricLabel A `MetricLabel` metadata represents a key-value pair that allows analyzing metrics at an additional level of granularity. For example, if a metric tracks the response time of a service, labels could be used to create separate versions that track response times for different clients. ZIO Metrics has a _label based dimensional_ data model where we have a metric name and just a list of key-value pairs attached to that metric name. So labels are the first-class citizen in ZIO Metrics. In monitoring dashboards, we can find or filter metrics using these labels. For example, we can append following labels (dimensions) to our metric aspects: - Endpoint (/api/users, /api/documents) - HTTP Method (POST, GET) - Deployment Environment (Staging, Production) - HTTP Response Code - Error code (404, 503) - Datacenter Zone (us-east, eu-west) ```scala val counter = Metric.counter("http_requests") .tagged( MetricLabel("env", "production"), MetricLabel("method", "GET"), MetricLabel("endpoint", "/api/users"), MetricLabel("zone", "ap-northeast"), ) ``` By labeling metrics, we can query in a more granular way in monitoring dashboards, such as - How many requests have been sent to each endpoint in total? - In which zone are we about to violate SLAs? - Which endpoint has the most latency? --- ## Summary(Metrics) A `Summary` represents a sliding window of a time series along with metrics for certain percentiles of the time series, referred to as quantiles. Quantiles describe specified percentiles of the sliding window that are of interest. For example, if we were using a summary to track the response time for requests over the last hour then we might be interested in the 50th percentile, 90th percentile, 95th percentile, and 99th percentile for response times. ## Internals Similar to a [histogram](histogram.md) a summary also observes _Double_ values. While a histogram directly modifies the bucket counters and does not keep the individual samples, the summary keeps the observed samples in its internal state. To avoid the set of samples grow uncontrolled, the summary needs to be configured with a maximum age `t` and a maximum size `n`. To calculate the statistics, maximal `n` samples will be used, all of which are not older than `t`. Essentially, the set of samples is a _sliding window_ over the last observed samples matching the conditions above. A summary is used to calculate a set of quantiles over the current set of samples. A quantile is defined by a _Double_ value `q` with `0 <= q <= 1` and resolves to a `Double` as well. The value of a given quantile `q` is the maximum value `v` out of the current sample buffer with size `n` where at most `q * n` values out of the sample buffer are less or equal to `v`. Typical quantiles for observation are `0.5` (the median) and the `0.95`. Quantiles are very good for monitoring _Service Level Agreements_. The ZIO Metrics API also allows summaries to be configured with an error margin `e`. The error margin is applied to the count of values, so that a quantile `q` for a set of size `s` resolves to value `v` if the number `n` of values less or equal to `v` is `(1 -e)q * s <= n <= (1+e)q`. ## API ```scala object Metric { def summary( name: String, maxAge: Duration, maxSize: Int, error: Double, quantiles: Chunk[Double] ): Summary[Double] = ??? def summaryInstant( name: String, maxAge: Duration, maxSize: Int, error: Double, quantiles: Chunk[Double] ): Summary[(Double, java.time.Instant)] = } ``` ## Use Cases Like [histograms](histogram.md), summaries are used for _monitoring latencies_, but they don't require us to define buckets. So, summaries are the best choice in these situations: - When histograms are not proper for us, in terms of accuracy - When we can't use histograms, as we don't have a good estimation of the range of values - When we don't need to perform aggregation or averages across multiple instances, as the calculations are done on the application side ## Examples Create a summary that can hold `100` samples, the max age of the samples is `1 day` and the error margin is `3%`. The summary should report the `10%`, `50%` and `90%` Quantile. It can be applied to effects yielding an `Int`: ```scala val summary: Summary[Double] = Metric.summary( name = "mySummary", maxAge = 1.day, maxSize = 100, error = 0.03d, quantiles = Chunk(0.1, 0.5, 0.9) ) ``` Now we can apply this aspect to an effect producing an `Int`: ```scala Random.nextDoubleBetween(100, 500) @@ summary ``` --- ## Supervisor A `Supervisor[A]` is allowed to supervise the launching and termination of fibers, producing some visible value of type `A` from the supervision. ## Creation ### track The `track` creates a new supervisor that tracks children in a set. It takes a boolean `weak` parameter as input, which indicates whether track children in a `Weakset` or not. ```scala val supervisor = Supervisor.track(true) // supervisor: zio.package.UIO[Supervisor[zio.Chunk[zio.Fiber.Runtime[Any, Any]]]] = Sync( // trace = "repl.MdocSession.MdocApp.supervisor(supervisor.md:14)", // eval = zio.Supervisor$$$Lambda$20126/0x00007f02a7055b18@319d8c00 // ) ``` We can periodically, report the status of the fibers of our program with the help of the Supervisor. ### fibersIn The `fibersIn` creates a new supervisor with an initial sorted set of fibers. In the following example we are creating a new supervisor from an initial set of fibers: ```scala def fiberListSupervisor = for { ref <- ZIO.succeed(new AtomicReference(SortedSet.from(fibers))) s <- Supervisor.fibersIn(ref) } yield (s) ``` ## Supervising Whenever we need to supervise a ZIO effect, we can call `ZIO#supervised` function, `supervised` takes a supervisor and return another effect. The behavior of children fibers is reported to the provided supervisor: ```scala val supervised = supervisor.flatMap(s => fib(20).supervised(s)) ``` Now we can access all information of children fibers through the supervisor. ## Example In the following example we are going to periodically monitor the number of fibers throughout our application life cycle: ```scala object SupervisorExample extends ZIOAppDefault { def run = for { supervisor <- Supervisor.track(true) fiber <- fib(20).supervised(supervisor).fork policy = Schedule .spaced(500.milliseconds) .whileInputZIO[Any, Unit](_ => fiber.status.map(_ != Status.Done)) logger <- monitorFibers(supervisor) .repeat(policy).fork _ <- logger.join result <- fiber.join _ <- Console.printLine(s"fibonacci result: $result") } yield () def monitorFibers(supervisor: Supervisor[Chunk[Fiber.Runtime[Any, Any]]]) = for { length <- supervisor.value.map(_.length) _ <- Console.printLine(s"number of fibers: $length") } yield () def fib(n: Int): ZIO[Any, Nothing, Int] = if (n <= 1) { ZIO.succeed(1) } else { for { _ <- ZIO.sleep(500.milliseconds) fiber1 <- fib(n - 2).fork fiber2 <- fib(n - 1).fork v2 <- fiber2.join v1 <- fiber1.join } yield v1 + v2 } } ``` --- ## Introduction to Tracing in ZIO Although logs and metrics are useful for understanding the behavior of individual services, they are not enough to provide a complete overview of the lifetime of a request in a distributed system. In a distributed system, a request can span multiple services and each service can make multiple requests to other services to fulfill the request. In such a scenario, we need to have a way to track the lifetime of a request across multiple services to diagnose what services are the bottlenecks and where the request is spending most of its time. ZIO Telemetry supports tracing through the OpenTelemetry API. To learn more about tracing, please refer to the [ZIO Telemetry documentation](https://zio.dev/zio-telemetry/opentracing). --- ## Introduction to Resource Management in ZIO When we are writing a long-lived application, resource management is very important. Proper resource management is vital to any large-scale application. We need to make sure that our application is resource-safe, and it doesn't leak any resource. Leaking socket connections, database connections or file descriptors is not acceptable in a web application. ZIO provides some good construct to make sure about this concern. To write a resource-safe application, we need to make sure whenever we are opening a resource, we have a mechanism to close that resource whether we use that resource completely or not, for example, an exception occurred during resource usage. ## Try / Finally Before we dive into the ZIO solution, it's better to review the `try` / `finally` which is the standard approach in the Scala language to manage resources. Scala has a `try` / `finally` construct which helps us to make sure we don't leak resources because no matter what happens in the try, the `finally` block will be executed. So we can open files in the try block, and then we can close them in the `finally` block, and that gives us the guarantee that we will not leak resources. Assume we want to read a file and return the number of its lines: ```scala def lines(file: String): Task[Long] = ZIO.attempt { def countLines(br: BufferedReader): Long = br.lines().count() val bufferedReader = new BufferedReader( new InputStreamReader(new FileInputStream("file.txt")), 2048 ) val count = countLines(bufferedReader) bufferedReader.close() count } ``` What happens if after opening the file and before closing the file, an exception occurs? So, the `bufferedReader.close()` line, doesn't have a chance to close the resource. This creates a resource leakage. The Scala language has `try...finally` construct, which helps up to prevent these situations. Let's rewrite the above example with `try..finally`: ```scala def lines(file: String): Task[Long] = ZIO.attempt { def countLines(br: BufferedReader): Long = br.lines().count() val bufferedReader = new BufferedReader( new InputStreamReader(new FileInputStream("file.txt")), 2048 ) try countLines(bufferedReader) finally bufferedReader.close() } ``` Now, we are sure that if our program is interrupted during the process of a file, the `finally` block will be executed. The `try` / `finally` solve simple problems, but it has some drawbacks: 1. It's not composable; We can't compose multiple resources together. 2. When we have multiple resources, we end up with messy and ugly code, hard to reason about, and refactoring. 3. We don't have any control over the order of resource clean-up 4. It only helps us to handle resources sequentially. It can't compose multiple resources, concurrently. 5. It doesn't support asynchronous workflows. 6. It's a manual way of resource management, not automatic. To have a resource-safe application we need to manually check that all resources are managed correctly. This way of resource management is error-prone in case of forgetting to manage resources, correctly. ## ZIO Solution ZIO's resource management features work across synchronous, asynchronous, concurrent, and other effect types, and provide strong guarantees even in the presence of failure, interruption, or defects in the application. ZIO has two major mechanisms to manage resources. ### Acquire Release ZIO generalized the pattern of `try` / `finally` and encoded it in `ZIO.acquireRelease` or `ZIO#acquireRelease` operations. Every acquire release requires three actions: 1. **Acquiring Resource**— An effect describing the acquisition of resource. For example, opening a file. 2. **Using Resource**— An effect describing the actual process to produce a result. For example, counting the number of lines in a file. 3. **Releasing Resource**— An effect describing the final step of releasing or cleaning up the resource. For example, closing a file. ```scala def use(resource: Resource): Task[Any] = ZIO.attempt(???) def release(resource: Resource): UIO[Unit] = ZIO.succeed(???) def acquire: Task[Resource] = ZIO.attempt(???) val result: Task[Any] = ZIO.acquireReleaseWith(acquire)(release)(use) ``` The acquire release guarantees us that the `acquiring` and `releasing` of a resource will not be interrupted. These two guarantees ensure us that the resource will always be released. Let's try a real example. We are going to write a function which count line number of given file. As we are working with file resource, we should separate our logic into three part. At the first part, we create a `BufferedReader`. At the second, we count the file lines with given `BufferedReader` resource, and at the end we close that resource: ```scala def lines(file: String): Task[Long] = { def countLines(reader: BufferedReader): Task[Long] = ZIO.attempt(reader.lines().count()) def releaseReader(reader: BufferedReader): UIO[Unit] = ZIO.succeed(reader.close()) def acquireReader(file: String): Task[BufferedReader] = ZIO.attempt(new BufferedReader(new FileReader(file), 2048)) ZIO.acquireReleaseWith(acquireReader(file))(releaseReader)(countLines) } ``` Let's write another function which copy a file from source to destination file. We can do that by nesting two acquire releases one for the `FileInputStream` and the other for `FileOutputStream`: ```scala def is(file: String): Task[FileInputStream] = ZIO.attempt(???) def os(file: String): Task[FileOutputStream] = ZIO.attempt(???) def close(resource: Closeable): UIO[Unit] = ZIO.succeed(???) def copy(from: FileInputStream, to: FileOutputStream): Task[Unit] = ??? def transfer(src: String, dst: String): ZIO[Any, Throwable, Unit] = { ZIO.acquireReleaseWith(is(src))(close) { in => ZIO.acquireReleaseWith(os(dst))(close) { out => copy(in, out) } } } ``` As there isn't any dependency between our two resources (`is` and `os`), it doesn't make sense to use nested acquire releases, so let's `zip` these two acquisition into one effect, and then use one acquire release on them: ```scala def transfer(src: String, dst: String): ZIO[Any, Throwable, Unit] = ZIO.acquireReleaseWith { is(src).zipPar(os(dst)) } { case (in, out) => ZIO.succeed(in.close()).zipPar(ZIO.succeed(out.close())) } { case (in, out) => copy(in, out) } ``` While using acquire release is a nice and simple way of managing resources, but there are some cases where an acquire release is not the best choice: 1. Acquire release is not composable— When we have multiple resources, composing them with an acquire release is not straightforward. 2. Messy nested acquire releases — In the case of multiple resources, nested acquire releases remind us of callback hell awkwardness. The acquire release is designed with nested resource acquisition. In the case of multiple resources, we encounter inefficient nested acquire release calls, and it causes refactoring a complicated process. Using acquire releases is simple and straightforward, but in the case of multiple resources, it isn't a good player. This is where we need another abstraction to cover these issues. ### Scope `Scope` is a composable data type for resource management, which wraps the acquisition and release action of a resource. We can think of `Scope` as a handle with built-in acquisition and release logic. To create a scoped resource, we need to provide `acquire` and `release` action of that resource to the `acquireRelease` constructor: ```scala val scoped = ZIO.acquireRelease(acquire)(release) ``` We can use scoped resources by calling `scoped` on that. A scoped resource is meant to be used only inside of the `scoped` block. So that resource is not available outside of the `scoped` block. Let's try to rewrite a `transfer` example with `Scope`: ```scala def transfer(from: String, to: String): ZIO[Any, Throwable, Unit] = { val resource = for { from <- ZIO.acquireRelease(is(from))(close) to <- ZIO.acquireRelease(os(to))(close) } yield (from, to) ZIO.scoped { resource.flatMap { case (in, out) => copy(in, out) } } } ``` --- ## Scope The `Scope` data type is the foundation of safe and composable resources handling in ZIO. Conceptually, a scope represents the lifetime of one or more resources. The resources can be used in the scope and are guaranteed to be released when the scope is closed. The `Scope` data type takes this idea and represents it as a first class value. ```scala trait Scope { def addFinalizerExit(finalizer: Exit[Any, Any] => UIO[Any]): UIO[Unit] def close(exit: => Exit[Any, Any]): UIO[Unit] } object Scope { def make: UIO[Scope] = ??? } ``` The `addFinalizerExit` operator lets us add a finalizer to the `Scope`. Based on the `Exit` value that the `Scope` is closed with, the finalizer will be run. The finalizer is guaranteed to be run when the scope is closed. The `close` operator closes the scope, running all the finalizers that have been added to the scope. It takes an `Exit` value and runs the finalizers based on that value. In the following example, we create a `Scope`, add a finalizer to it, and then close the scope: ```scala for { scope <- Scope.make _ <- ZIO.debug("Scope is created!") _ <- scope.addFinalizer( for { _ <- ZIO.debug("The finalizer is started!") _ <- ZIO.sleep(5.seconds) _ <- ZIO.debug("The finalizer is done!") } yield () ) _ <- ZIO.debug("Leaving scope!") _ <- scope.close(Exit.succeed(())) _ <- ZIO.debug("Scope is closed!") } yield () ``` The output of this program will be: ``` Scope is created! Leaving scope! The finalizer is started! The finalizer is done! Scope is closed! ``` We can see that the finalizer is run after we called `close` on the scope. So the finalizer is guaranteed to be run when the scope is closed. The `Scope#extend` operator, takes a `ZIO` effect that requires a `Scope` and provides it with a `Scope` without closing it afterwards. This allows us to extend the lifetime of a scoped resource to the lifetime of a scope. ## Scopes and The ZIO Environment In combination with the ZIO environment, `Scope` gives us an extremely powerful way to manage resources. We can define a resource using operators such as `ZIO.acquireRelease`, which lets us construct a scoped value from an `acquire` and `release` workflow. For example, here is how we might define a simple resource: ```scala def acquire(name: => String): ZIO[Any, IOException, Source] = ZIO.attemptBlockingIO(Source.fromFile(name)) def release(source: => Source): ZIO[Any, Nothing, Unit] = ZIO.succeedBlocking(source.close()) def source(name: => String): ZIO[Scope, IOException, Source] = ZIO.acquireRelease(acquire(name))(release(_)) ``` Notice that the `acquireRelease` operator added a `Scope` to the environment required by the workflow. This indicates that this workflow needs a `Scope` to be run and will add a finalizer that will close the resource when the scope is closed. We can continue working with the resource as long as we want by using `flatMap` or other `ZIO` operators. For example, here is how we might read the contents of a file: ```scala source("cool.txt").flatMap { source => ZIO.attemptBlockingIO(source.getLines()) } // res2: ZIO[Scope, IOException, Iterator[String]] = FlatMap( // trace = "repl.MdocSession.MdocApp.res2(scope.md:85)", // first = DynamicNoBox( // trace = "repl.MdocSession.MdocApp.source(scope.md:79)", // update = 1L, // f = zio.ZIO$$$Lambda$20094/0x00007f02a70292e8@3bc37587 // ), // successK = // ) ``` Once we are finished working with the file, we can close the scope using the `ZIO.scoped` operator. This function creates a new `Scope`, provides it to the workflow, and closes the `Scope` once the workflow is complete: ```scala object ZIO { def scoped[R, E, A](zio: ZIO[Scope with R, E, A]): ZIO[R, E, A] = ??? } ``` The `scoped` operator removes the `Scope` from the environment, indicating that there are no longer any resources used by this workflow that require a scope. We now have a workflow that is ready to run: ```scala def contents(name: => String): ZIO[Any, IOException, Chunk[String]] = ZIO.scoped { source(name).flatMap { source => ZIO.attemptBlockingIO(Chunk.fromIterator(source.getLines())) } } ``` In some cases ZIO applications may provide a `Scope` for us for resources that we don't specify a scope for. For example `ZIOApp` provides a `Scope` for our entire application and ZIO Test provides a `Scope` for each test. :::note Please note that like any other services that we can obtain from the ZIO environment, we can do the same with `Scope`. By calling `ZIO.service[Scope]` we can obtain the `Scope` service and then use it to manage resources by adding finalizers to it: ```scala val resourcefulApp: ZIO[Scope, Nothing, Unit] = for { scope <- ZIO.service[Scope] _ <- ZIO.debug("Entering the scope!") _ <- scope.addFinalizer( for { _ <- ZIO.debug("The finalizer is started!") _ <- ZIO.sleep(5.seconds) _ <- ZIO.debug("The finalizer is done!") } yield () ) _ <- ZIO.debug("Leaving scope!") } yield () ``` Then we can run the `app` workflow by providing the `Scope` service to it: ```scala val finalApp: ZIO[Any, Nothing, Unit] = Scope.make.flatMap(scope => resourcefulApp.provide(ZLayer.succeed(scope)).onExit(scope.close(_))) ``` Here is the output of the program: ``` Entering the scope! Leaving scope! The finalizer is started! The finalizer is done! ``` So we can think of `Scope` as a service that helps us manage resources effectfully. However, the way we utilized it in the previous example is not as per the best practices, and it was only for educational purposes. In real-world applications, we can easily manage resources by utilizing high-level operators such as `ZIO.acquireRelease` and `ZIO.scoped`. ::: ## Scopes are Dynamic One important thing to note about `Scope` is that they are dynamic. This means that if we have an effect that requires a `Scope` we can `flatMap` over that effect and use its value to create a new effect. The new effect extends the lifetime of the original scope. So as we don't close the scope (by calling `ZIO.scoped`) the resources will not be released, and they can become bigger and bigger until we close them: ```scala ZIO.scoped { file("path/to/file.txt").flatMap(getLines).flatMap(processLines) } ``` ## Defining Resources We have already seen the `acquireRelease` operator, which is one of the most fundamental operators for creating scoped resources. ```scala object ZIO { def acquireRelease[R, E, A](acquire: => ZIO[R, E, A])(release: A => ZIO[R, Nothing, Any]): ZIO[R with Scope, E, A] = ??? } ``` The `acquireRelease` operator performs the `acquire` workflow uninterruptibly. This is important because if we allowed interruption during resource acquisition we could be interrupted when the resource was partially acquired. The guarantee of the `acquireRelease` operator is that if the `acquire` workflow successfully completes execution then the `release` workflow is guaranteed to be run when the `Scope` is closed. In addition to the `acquireRelease` operator, there is a more powerful variant called `acquireReleaseExit` that lets the finalizer depend on the `Exit` value that the `Scope` is closed with. This can be useful if we want to run a different finalizer depending on whether the `Scope` was closed with a success or a failure. ```scala object ZIO { def acquireReleaseExit[R, E, A](acquire: => ZIO[R, E, A])(release: (A, Exit[Any, Any]) => ZIO[R, Nothing, Any]): ZIO[R with Scope, E, A] = ??? } ``` There is also another family of operators to be aware of that allow the `acquire` workflow to be interrupted. ```scala object ZIO { def acquireReleaseInterruptible[R, E, A](acquire: => ZIO[R, E, A])(release: ZIO[R, Nothing, Any]): ZIO[R with Scope, E, A] = ??? def acquireReleaseInterruptibleExit[R, E, A](acquire: => ZIO[R, E, A])(release: Exit[Any, Any] => ZIO[R, Nothing, Any]): ZIO[R with Scope, E, A] = ??? } ``` In this case the `release` workflow is not allowed to depend on the resource, since the `acquire` workflow might be interrupted after partially acquiring the resource. The `release` workflow is responsible for independently determining what finalization is required, for example by inspecting in-memory state. This is a more advanced variant so we should generally use the standard `acquireRelease` operator. However, the `acquireReleaseInterruptible` operator can be very useful to describe more advanced resource acquisition scenarios where part of the acquisition can be interruptible. ## Converting Resources Into Other ZIO Data Types We will commonly want to convert scoped resources into other ZIO data types, particularly `ZLayer` for dependency injection and `ZStream`, `ZSink`, and `ZChannel` for streaming. We can easily do this using the `scoped` constructor on each of these data types. For example, here is how we might convert the `source` resource above into a `ZStream` of the contents: ```scala def lines(name: => String): ZStream[Any, IOException, String] = ZStream.scoped(source(name)).flatMap { source => ZStream.fromIteratorSucceed(source.getLines()) } ``` Just like the `scoped` operator on `ZIO`, the `scoped` operator on `ZStream` removes the `Scope` from the environment, indicating that there are no longer any resources used by this workflow which require a scope. The lifetime of these resources will now be governed by the lifetime of the stream, which generally means that the resources will be released as soon as we are done pulling from the stream. This lets the lifetime of these resources be managed by various stream operators to release those resources as efficiently as possible, for example releasing resources associated with each stream as soon as we are done with that stream when we merge two streams. Similarly, we can convert a scoped resource into a `ZLayer` by using the `scoped` constructor on `ZLayer`: ```scala def sourceLayer(name: => String): ZLayer[Any, IOException, Source] = ZLayer.scoped(source(name)) ``` Again, the `Scope` has been removed from the environment, indicating that the lifetime of this resource will no longer be governed by the `Scope` but by the lifetime of the layer. In this case, that means the resource will be released as soon as the workflow that the layer is provided to completes execution, whether by success, failure, or interruption. We should generally use the `scoped` operators on other ZIO data types to convert a scoped resource into a value of that data type. Having the lifetime of resources governed by the lifetime of those data types makes our code simpler and easier to reason about. ## Controlling Finalizer Ordering By default, when a `Scope` is closed all finalizers added to that `Scope` will be closed in the reverse of the order in which those finalizers were added to the `Scope`. Releasing resources in the reverse order in which they were acquired makes sense because a resource that was acquired first may be necessary for a later acquired resource to be closed. For example, if we open a network connection and then open a file on a remote server we need to close the file before closing the network connection. Otherwise we would no longer be able to interact with the remote server to close the file! Therefore, in most cases we don't have to do anything with regard to order of finalizers. However, in some cases we may want to run finalizers in parallel instead of sequentially, for example when the resources were also acquired in parallel. For this we can use the `ZIO.parallelFinalizers` operator to indicate that finalizers should be run in parallel instead of sequentially when a scope is closed. Here is how we could use it to implement an operator that acquires and releases two resources in parallel. ```scala def zipScoped[R <: Scope, E, A, B]( left: ZIO[R, E, A], right: ZIO[R, E, B] ): ZIO[R, E, (A, B)] = ZIO.parallelFinalizers(left.zipPar(right)) ``` The `zipPar` operator on `ZIO` takes care of acquiring the resources in parallel and the `parallelFinalizers` operator handles releasing them in parallel. This makes it easy for us to do parallel resource acquisition by leveraging the powerful concurrency operators that already exist on `ZIO`. ## Advanced Scope Operators So far we have seen that while `Scope` is the foundation of safe and composable resource handling in ZIO, we don't actually need to work with the `Scope` data type directly other than being able to inpect the type signature to see if a workflow is scoped. In most cases we just use the `acquireRelease` constructor or one of its variants to construct our resource and either work with the resource and close its scope using `ZIO.scoped` or convert the resource into another ZIO data type using an operator such as `ZStream.scoped` or `ZLayer.scoped`. However, for more advanced use cases we may need to work with scopes directly and `Scope` has several useful operators for helping us do so. ### Using a Scope First, we can `use` a `Scope` by providing it to a workflow that needs a `Scope` and closing the `Scope` immediately after. This is analogous to the `ZIO.scoped` operator: ```scala trait Closeable extends Scope { def use[R, E, A](zio: => ZIO[R with Scope, E, A]): ZIO[R, E, A] } object ZIO { def scoped[R, E, A](zio: => ZIO[R with Scope, E, A]): ZIO[R, E, A] = ??? } ``` In the following example, we obtained a `Scope` and added a finalizer to it, and then extended its lifetime to the lifetime of the `resource1` and `resource2`: ```scala object ExtendingScopesExample extends ZIOAppDefault { val resource1: ZIO[Scope, Nothing, Unit] = ZIO.acquireRelease(ZIO.debug("Acquiring the resource 1"))(_ => ZIO.debug("Releasing the resource one") *> ZIO.sleep(5.seconds) ) val resource2: ZIO[Scope, Nothing, Unit] = ZIO.acquireRelease(ZIO.debug("Acquiring the resource 2"))(_ => ZIO.debug("Releasing the resource two") *> ZIO.sleep(3.seconds) ) def run = ZIO.scoped( for { scope <- ZIO.scope _ <- ZIO.debug("Entering the main scope!") _ <- scope.addFinalizer(ZIO.debug("Releasing the main resource!") *> ZIO.sleep(2.seconds)) _ <- scope.extend(resource1) _ <- scope.extend(resource2) _ <- ZIO.debug("Leaving scope!") } yield () ) } ``` output: ``` Entering the main scope! Acquiring the resource 1 Acquiring the resource 2 Leaving scope! Releasing the resource two Releasing the resource one Releasing the main resource! ``` ### Extending a Scope Second, we can use the `extend` operator on `Scope` to provide a workflow with a scope without closing it afterwards. This allows us to extend the lifetime of a scoped resource to the lifetime of a scope, effectively allowing us to "extend" the lifetime of that resource: ```scala trait Scope { def extend[R, E, A](zio: => ZIO[Scope with R, E, A]): ZIO[R, E, A] } ``` ### Closing a Scope Third, we can `close` a `Scope`. One thing to note here is that by default only the creator of a `Scope` can close it: ```scala trait Closeable extends Scope { def close(exit: => Exit[Any, Any]): UIO[Unit] } ``` Creating a new `Scope` returns a `Scope.Closeable` which can be closed. Normally users of a `Scope` will only be provided with a `Scope` which does not expose a `close` operator. This way the creator of a `Scope` can be sure that someone else will not "pull the rug out from under them" by closing the scope prematurely. --- ## ScopedRef: Mutable Reference For Resources `ScopedRef` is a resourceful version of `Ref` data type. So it is a `Ref` for resourceful effects. ## Operations There are two basic operations: get and set: - `ScopedRef#get` returns the current value of the scoped ref. - `ScopedRef#set` sets the scoped ref to a new value by acquiring the new resource to create a new value of the scoped ref. Setting a new value releases the old resource automatically. ## Construction The `ScopedRef` has two constructors: ```scala object ScopedRef { def make[A](a: => A): ZIO[Scope, Nothing, ScopedRef[A]] = ??? def fromAcquire[R, E, A](acquire: ZIO[R, E, A]): ZIO[R with Scope, E, ScopedRef[A]] = ??? } ``` So we have two options to create a `ScopedRef`: - `ScopedRef.make` creates a scoped ref from an ordinary value. We can use this constructor when we don't need to acquire a resource to create a value of the scoped ref, for example, when we have a constant value. - `ScopedRef.fromAcquire` creates a scoped ref from an effect that resourcefully produces a value. :::note `ScopedRef` is resourceful, so its lifetimes is scoped. Whenever we don't need it anymore, we can release it by using `ZIO#scoped` combinator. ::: ## Example Let's see how changing the value of a `ScopedRef` automatically releases the old resource: ```scala object MainApp extends ZIOAppDefault { def run = for { _ <- ZIO.unit r1 = ZIO.acquireRelease( ZIO .debug("acquiring the first resource") .as(5) )(_ => ZIO.debug("releasing the first resource")) r2 = ZIO.acquireRelease( ZIO .debug("acquiring the second resource") .as(10) )(_ => ZIO.debug("releasing the second resource")) sref <- ScopedRef.fromAcquire(r1) _ <- sref.get.debug _ <- sref.set(r2) _ <- sref.get.debug } yield () } ``` The output: ``` acquiring the first resource 5 acquiring the second resource releasing the first resource 10 releasing the second resource ``` ## Interruption The `set` operation runs in an uninterruptible block. This means that if you want to manage a `Fiber` using a `ScopedRef` by using `forkScoped`, this fiber must be explicitly made interruptible. --- ## ZKeyedPool The `ZKeyedPool[+Err, -Key, Item]` is a pool of items of type `Item` that are associated with a key of type `Key`. An attempt to get an item from a pool may fail with an error of type `Err`. The interface is similar to [`ZPool`](zpool.md), but it allows associating items with keys: ```scala trait ZKeyedPool[+Err, -Key, Item] { def get(key: Key): ZIO[Scope, Err, Item] def invalidate(item: Item): UIO[Unit] } ``` The two fundamental operators on a `ZPool` is `get` and `invalidate`: - The `get` operator retrieves an item associated with the given key from the pool in a scoped effect. - The `invalidate` operator invalidates the specified item. This will cause the pool to eventually reallocate the item. There couple of ways to create a `ZKeyedPool`: Generally there are two ways to create a `ZKeyedPool`: 1. Fixed-size Pools 2. Dynamic-size Pools ### Fixed-size Pools 1. We can create a pool that has a fixed number of items for each key: ```scala object ZKeyedPool { def make[Key, Env: EnvironmentTag, Err, Item]( get: Key => ZIO[Env, Err, Item], size: => Int ): ZIO[Env with Scope, Nothing, ZKeyedPool[Err, Key, Item]] = ??? } ``` For example The `ZKeyedPool.make(key => resource(key), 3)` creates a pool of resources where each key has a pool of size 3: ```scala object ZKeyedPoolExample extends ZIOAppDefault { def resource(key: String): ZIO[Scope, Nothing, String] = ZIO.acquireRelease( ZIO.random .flatMap(_.nextUUID.map(_.toString)) .flatMap(uuid => ZIO.debug(s"Acquiring the resource with the $key key and the $uuid id").as(uuid)) )(uuid => ZIO.debug(s"Releasing the resource with the $key key and the $uuid id!")) def run = for { pool <- ZKeyedPool.make(resource, 3) _ <- pool.get("foo") item <- pool.get("bar") _ <- ZIO.debug(s"Item: $item") } yield () } ``` Here is an example output of the above code: ``` Acquiring the resource with the foo key and the 82ee3cab-7f4c-47f1-b3e6-0cd49035925d id! Acquiring the resource with the foo key and the f9cd881f-fa2e-421c-a6ae-c8d16f6b4500 id! Acquiring the resource with the foo key and the 09a8f4c9-24ee-411c-b1d0-958479266cb0 id! Acquiring the resource with the bar key and the 4d6f9c95-8d72-4560-bc20-0965b547cfb7 id! Acquiring the resource with the bar key and the 44bf6641-bb0f-4088-989b-95fb442d93ab id! Acquiring the resource with the bar key and the fc2780a7-1717-4027-b201-65441168bfce id! Item: 4d6f9c95-8d72-4560-bc20-0965b547cfb7 Releasing the resource with the bar key and the fc2780a7-1717-4027-b201-65441168bfce id! Releasing the resource with the bar key and the 44bf6641-bb0f-4088-989b-95fb442d93ab id! Releasing the resource with the bar key and the 4d6f9c95-8d72-4560-bc20-0965b547cfb7 id! Releasing the resource with the foo key and the 09a8f4c9-24ee-411c-b1d0-958479266cb0 id! Releasing the resource with the foo key and the f9cd881f-fa2e-421c-a6ae-c8d16f6b4500 id! Releasing the resource with the foo key and the 82ee3cab-7f4c-47f1-b3e6-0cd49035925d id! ``` 2. We can create a pool that has a fixed number of items but with different pool size for each key: ```scala object ZKeyedPool { def make[Key, Env: EnvironmentTag, Err, Item]( get: Key => ZIO[Env, Err, Item], size: Key => Int ): ZIO[Env with Scope, Nothing, ZKeyedPool[Err, Key, Item]] = ??? } ``` In the following example, we have created a pool of resources where based on the key, the pool size for that key is different, the pool size for keys starting with "foo" is 2, and for keys starting with "bar" is 3, and for all other keys, the pool size is 1: ```scala for { pool <- ZKeyedPool.make(resource, (key: String) => key match { case k if k.startsWith("foo") => 2 case k if k.startsWith("bar") => 3 case _ => 1 }) _ <- pool.get("foo1") item <- pool.get("bar1") _ <- ZIO.debug(s"Item: $item") } yield () ``` Here is an example output of the above code: ``` Acquiring the resource with foo1 key and 052778eb-31c2-4eac-806b-46651813b457 id Acquiring the resource with foo1 key and bd39dbe4-8f43-4376-a209-5af8ca118af2 id Acquiring the resource with bar1 key and ecfc80da-c8b2-4726-813c-259748a98c3e id Acquiring the resource with bar1 key and 0ddfd051-7bf8-4596-a7b9-4011ceeb0976 id Acquiring the resource with bar1 key and 67239ac8-5def-45ac-962f-b05fb82bf0c3 id Item: ecfc80da-c8b2-4726-813c-259748a98c3e Releasing the resource with bar1 key and 67239ac8-5def-45ac-962f-b05fb82bf0c3 id! Releasing the resource with bar1 key and 0ddfd051-7bf8-4596-a7b9-4011ceeb0976 id! Releasing the resource with bar1 key and ecfc80da-c8b2-4726-813c-259748a98c3e id! Releasing the resource with foo1 key and bd39dbe4-8f43-4376-a209-5af8ca118af2 id! Releasing the resource with foo1 key and 052778eb-31c2-4eac-806b-46651813b457 id! ``` ### Dynamic-size Pools 1. We can create a pool with the specified minimum and maximum sized and time to live before a pool whose excess items are not being used will be shrunk down to the minimum size: ```scala object ZKeyedPool { def make[Key, Env: EnvironmentTag, Err, Item]( get: Key => ZIO[Env, Err, Item], range: Key => Range, timeToLive: Duration ): ZIO[Env with Scope, Nothing, ZKeyedPool[Err, Key, Item]] = ??? } ``` 2. Similarly, we can create a pool of resources where the minimum and maximum size of the pool is different for each key. Also, the time to live for each key can be different: ```scala def make[Key, Env: EnvironmentTag, Err, Item]( get: Key => ZIO[Env, Err, Item], range: Key => Range, timeToLive: Key => Duration ): ZIO[Env with Scope, Nothing, ZKeyedPool[Err, Key, Item]] = ??? ``` --- ## ZPool A `ZPool[E, A]` is a pool of items of type `A`, each of which may be associated with the acquisition and release of resources. An attempt to get an item `A` from a pool may fail with an error of type `E`. ## Motivation Acquiring some resources is expensive to create and time-consuming. This includes network connections (sockets, databases, remote services), threads, and so on. There are some cases that - We require a **fast and predictable** way of accessing resources. - We need a solution to **scale across the number of resources**. - On the other hand, each **resource consumption doesn't take a long time**. If we create a new resource for every resource acquisition, consequently we will find ourselves in a constant repetition of acquisition and release of resources. This might end up with thousands of resources (e.g. connection to a database) created within a short time, which will reduce the performance of our application. To address these issues, we can create a pool of pre-initialized resources: - Whenever we need a new resource, we acquire that from the existing resources of the pool. So the resource acquisition will be predictable, and it will avoid the overhead of acquisition. - When the resource is no longer needed, we release that back to the resource pool. So the released resources will be recyclable, and it will avoid the overhead of re-acquisition. `ZPool` is an implementation of such an idea with some excellent properties that we will cover on this page. ## Introduction `ZPool` is an asynchronous and concurrent generalized pool of reusable resources, that is used to create and manage a pool of objects. ```scala trait ZPool[+Error, Item] { def get: ZIO[Scope, Error, Item] def invalidate(item: Item): UIO[Unit] } ``` The two fundamental operators on a `ZPool` is `get` and `invalidate`: - The `get` operator retrieves an item from the pool in a scoped effect. - The `invalidate` operator invalidates the specified item. This will cause the pool to eventually reallocate the item. Let's assume we have a resource that is expensive to create and release. We want to access this resource frequently, but we don't want to create a new resource every time we need it. We can use `ZPool` to create a pool of resources and acquire them when only needed. ```scala object PoolExample extends ZIOAppDefault { def resource: ZIO[Scope, Nothing, UUID] = ZIO.acquireRelease( ZIO.random.flatMap(_.nextUUID).flatMap(uuid => ZIO.debug(s"Acquiring the resource: $uuid!").as(uuid)) )(uuid => ZIO.debug(s"Releasing the resource $uuid!")) def run = for { pool <- ZPool.make(resource, 3) item <- pool.get _ <- ZIO.debug(s"Item: $item") } yield () } ``` In this example, we created a pool of 3 resources. When we call `pool.get`, it will return one of the resources from the pool. After calling the `pool.get` three times, if we call it again, it will block until one of the resources is released back to the pool using `ZPool#invalidate`. ## Constructing ZPools ### Fixed-size Pools The `make` constructor is a common way to create a `ZPool`: ```scala object ZPool { def make[E, A](get: ZIO[Scope, E, A], size: Int): ZIO[Scope, Nothing, ZPool[E, A]] = ??? } ``` It takes a scoped resource of type `A`, and the `size` of the pool. The return type will be a scoped `ZPool`. - A fixed pool size will be used to pre-allocate pool entries, so all the entries of the pool will be acquired eagerly. As a client of the `ZPool` it is recommended to analyze requirements to find out the best suitable size for the resource pool. If we set up a pool with too many eagerly-acquired resources, that may reduce the performance due to the resource contention. - As the return type of the constructor is `ZIO[Scope, Nothing, ZPool[E, A]]`, it will manage automatically the life cycle of the pool. So, as a client of `ZPool`, we do not require to shutdown the pool manually. There is another constructor called `ZPool.fromIterable` that is suitable when no cleanup or release actions are required. ### Dynamically-sized Pools The previous constructor creates a `ZPool` with a fixed size, so all of its entries are pre-allocated. There is another constructor that creates a pool with pre-allocated minimum entries (eagerly-acquired resources), plus it can increase its entries _on-demand_ (lazily-acquired resources) until reaches the specified maximum size: ```scala def make[E, A](get: ZIO[Scope, E, A], range: Range, // minimum and maximum size of the pool timeToLive: Duration): ZIO[Scope, Nothing, ZPool[E, A]] ``` Having a lot of resources that are over our average requirement can waste space and degrade the performance. Therefore, this variant of `ZPool` has an _eviction policy_. By taking the `timeToLive` argument, it will evict excess items that have not been acquired for more than the `timeToLive` time, until it reaches the minimum size. Here is an example of creating pool of database connections: ```scala ZIO.scoped { ZPool.make(acquireDbConnection, 10 to 20, 60.seconds).flatMap { pool => ZIO.scoped { pool.get.flatMap { conn => useConnection(conn) } } } } ``` ## Operators on ZPool ### Resource Acquisition After creating a pool, we can retrieve a resource from the pool using `ZPool#get` operation: ```scala trait ZPool[+Error, Item] { def get: ZIO[Scope, Error, Item] } ``` Here is how it works behind the scenes: - If there is any resource available in the pre-allocated entries, it will return one of those. - If the demand exceeds the available resources in the pool, one new resource will be allocated on-demand and returned to the client. - If the demand exceeds the maximum size of the pool, it will block while it waits for a resource to become available. - If the acquisition of a resource fails, then the returned effect will fail for the same reason. In case of failure, we can retry a failed acquisition. It will repeat the acquisition attempt: ```scala ZPool.make(acquireDbConnection, 10).flatMap { pool => pool.get.flatMap( conn => useConnection(conn)).eventually } ``` ### Resource Invalidation When we are working with resources, especially the remote ones, they may become invalid or faulty. On the other hand, when the resource's life cycle ends, it will be automatically returned to the resource pool. So what happens if another client acquires a resource from the pool? The faulty resource can cause a problem for the next client. To prevent that, we can use the `ZPool#invalidate` to claim that the resource is invalid: ```scala trait ZPool[+Error, Item] { def invalidate(item: Item): UIO[Unit] } ``` In this case, the pool will eventually reallocate the item, but this will happen lazily rather than eagerly. --- ## Built-in Schedules ## succeed Returns a schedule that repeats one time, producing the specified constant value: ```scala val constant = Schedule.succeed(5) ``` ## fromFunction A schedule that always recurs, mapping input values through the specified function: ```scala val inc = Schedule.fromFunction[Int, Int](_ + 1) ``` ## stop A schedule that does not recur, just stops and returns one `Unit` element: ```scala val stop = Schedule.stop ``` ## once A schedule that recurs one time an returns one `Unit` element: ```scala val once = Schedule.once ``` ## forever A schedule that always recurs and produces number of recurrence at each run: ```scala val forever = Schedule.forever ``` ## recurs A schedule that only recurs the specified number of times: ```scala val recurs = Schedule.recurs(5) ``` ## spaced A schedule that recurs continuously, each repetition spaced the specified duration from the last run: ```scala val spaced = Schedule.spaced(10.milliseconds) ``` ## fixed A schedule that recurs on a fixed interval. Returns the number of repetitions of the schedule so far: ```scala val fixed = Schedule.fixed(10.seconds) ``` ## exponential A schedule that recurs using exponential backoff: ```scala val exponential = Schedule.exponential(10.milliseconds) ``` ## fibonacci A schedule that always recurs, increasing delays by summing the preceding two delays (similar to the fibonacci sequence). Returns the current duration between recurrences: ```scala val fibonacci = Schedule.fibonacci(10.milliseconds) ``` ## identity A schedule that always decides to continue. It recurs forever, without any delay. `identity` schedule consumes input, and emit the same as output (`Schedule[Any, A, A]`): ```scala val identity = Schedule.identity[Int] ``` ## unfold A schedule that repeats one time from the specified state and iterator: ```scala val unfold = Schedule.unfold(0)(_ + 1) ``` --- ## Schedule Combinators Schedules define stateful, possibly effectful, recurring schedules of events, and compose in a variety of ways. Combinators allow us to take schedules and combine them together to get other schedules and if we have combinators with just the right properties. Then in theory we should be able to solve an infinite number of problems, with only a few combinators and few base schedules. ## Composition Schedules compose in the following primary ways: * **Union**. This performs the union of the intervals of two schedules. * **Intersection**. This performs the intersection of the intervals of two schedules. * **Sequencing**. This concatenates the intervals of one schedule onto another. ### Union Combines two schedules through union, by recurring if either schedule wants to recur, using the minimum of the two delays between recurrences. | | `s1` | `s2` | `s1` | | `s2` | |---------------------|---------------------|---------------------|---------------------------| | Type | `Schedule[R, A, B]` | `Schedule[R, A, C]` | `Schedule[R, A, (B, C)]` | | Continue: `Boolean` | `b1` | `b2` | `b1` | | `b2` | | Delay: `Duration` | `d1` | `d2` | `d1.min(d2)` | | Emit: `(A, B)` | `a` | `b` | `(a, b)` | We can combine two schedule through union with `||` operator: ```scala val expCapped = Schedule.exponential(100.milliseconds) || Schedule.spaced(1.second) ``` ### Intersection Combines two schedules through the intersection, by recurring only if both schedules want to recur, using the maximum of the two delays between recurrences. | | `s1` | `s2` | `s1 && s2` | |---------------------|---------------------|---------------------|--------------------------| | Type | `Schedule[R, A, B]` | `Schedule[R, A, C]` | `Schedule[R, A, (B, C)]` | | Continue: `Boolean` | `b1` | `b2` | `b1 && b2` | | Delay: `Duration` | `d1` | `d2` | `d1.max(d2)` | | Emit: `(A, B)` | `a` | `b` | `(a, b)` | We can intersect two schedule with `&&` operator: ```scala val expUpTo10 = Schedule.exponential(1.second) && Schedule.recurs(10) ``` ### Sequencing Combines two schedules sequentially, by following the first policy until it ends, and then following the second policy. | | `s1` | `s2` | `s1 andThen s2` | |-------------------|---------------------|---------------------|---------------------| | Type | `Schedule[R, A, B]` | `Schedule[R, A, C]` | `Schedule[R, A, C]` | | Delay: `Duration` | `d1` | `d2` | `d1 + d2` | | Emit: `B` | `a` | `b` | `b` | We can sequence two schedule by using `andThen`: ```scala val sequential = Schedule.recurs(10) andThen Schedule.spaced(1.second) ``` ## Piping Combine two schedules by piping the output of the first schedule to the input of the other. Effects described by the first schedule will always be executed before the effects described by the second schedule. | | `s1` | `s2` | `s1 >>> s2` | |-------------------|---------------------|---------------------|---------------------| | Type | `Schedule[R, A, B]` | `Schedule[R, B, C]` | `Schedule[R, A, C]` | | Delay: `Duration` | `d1` | `d2` | `d1 + d2` | | Emit: `B` | `a` | `b` | `b` | We can pipe two schedule by using `>>>` operator: ```scala val totalElapsed = Schedule.spaced(1.second) <* Schedule.recurs(5) >>> Schedule.elapsed ``` ## Jittering A `jittered` is a combinator that takes one schedule and returns another schedule of the same type except for the delay which is applied randomly: | Function | Input Type | Output Type | |------------|----------------------------|--------------------------------------| | `jittered` | | `Schedule[Env with Random, In, Out]` | | `jittered` | `min: Double, max: Double` | `Schedule[Env with Random, In, Out]` | We can jitter any schedule by calling `jittered` on it: ```scala val jitteredExp = Schedule.exponential(10.milliseconds).jittered ``` When a resource is out of service due to overload or contention, retrying and backing off doesn't help us. If all failed API calls are backed off to the same point of time, they cause another overload or contention. Jitter adds some amount of randomness to the delay of the schedule. This helps us to avoid ending up accidentally synchronizing and taking the service down by accident. The form with parameters `min` and `max` creates a new schedule where the new interval size is randomly distributed between `min * old interval` and `max * old interval`. [Research](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) shows that `Schedule.jittered(0.0, 1.0)` is very suitable for retrying. ## Collecting A `collectAll` is a combinator that when we call it on a schedule, produces a new schedule that collects the outputs of the first schedule into a chunk. | Function | Input Type | Output Type | |--------------|--------------------------|---------------------------------| | `collectAll` | `Schedule[Env, In, Out]` | `Schedule[Env, In, Chunk[Out]]` | In the following example, we are catching all recurrence of schedule into `Chunk`, so at the end, it would contain `Chunk(0, 1, 2, 3, 4)`: ```scala val collect = Schedule.recurs(5).collectAll ``` ## Filtering We can filter inputs or outputs of a schedule with `whileInput` and `whileOutput`. Alse ZIO schedule has an effectful version of these two functions, `whileInputZIO` and `whileOutputZIO`. | Function | Input Type | Output Type | |------------------|------------------------------|----------------------------| | `whileInput` | `In1 => Boolean` | `Schedule[Env, In1, Out]` | | `whileOutput` | `Out => Boolean` | `Schedule[Env, In, Out]` | | `whileInputZIO` | `In1 => URIO[Env1, Boolean]` | `Schedule[Env1, In1, Out]` | | `whileOutputZIO` | `Out => URIO[Env1, Boolean]` | `Schedule[Env1, In, Out]` | In following example we collect all emiting outputs before reaching the 5 output, so it would return `Chunk(0, 1, 2, 3, 4)`: ```scala val res = Schedule.unfold(0)(_ + 1).whileOutput(_ < 5).collectAll ``` ## Mapping There are two versions for mapping schedules, `map` and its effectful version `mapZIO`. | Function | Input Type | Output Type | |----------|------------------------------|----------------------------| | `map` | `f: Out => Out2` | `Schedule[Env, In, Out2]` | | `mapZIO` | `f: Out => URIO[Env1, Out2]` | `Schedule[Env1, In, Out2]` | ## Left/Right Ap Sometimes when we intersect two schedules with the `&&` operator, we just need to ignore the left or the right output. - * `*>` ignore the left output - * `<*` ignore the right output ## Modifying Modifies the delay of a schedule: ```scala val boosted = Schedule.spaced(1.second).delayed(_ => 100.milliseconds) ``` ## Tapping Whenever we need to effectfully process each schedule input/output, we can use `tapInput` and `tapOutput`. We can use these two functions for logging purposes: ```scala val tappedSchedule = Schedule.count.whileOutput(_ < 5).tapOutput(o => Console.printLine(s"retrying $o").orDie) ``` --- ## Examples(Schedule) Let's try some example of creating and combining schedules. 1. Stop retrying after a specified amount of time has elapsed: ```scala val expMaxElapsed = (Schedule.exponential(10.milliseconds) >>> Schedule.elapsed).whileOutput(_ < 30.seconds) ``` 2. Retry only when a specific exception occurs: ```scala val whileTimeout = Schedule.exponential(10.milliseconds) && Schedule.recurWhile[Throwable] { case _: TimeoutException => true case _ => false } ``` --- ## Introduction to Scheduling ZIO Effects A `Schedule[Env, In, Out]` is an **immutable value** that **describes** a recurring effectful schedule, which runs in some environment `Env`, after consuming values of type `In` (errors in the case of `retry`, or values in the case of `repeat`) produces values of type `Out`, and in every step based on input values and the internal state decides to halt or continue after some delay **d**. Schedules are defined as a possibly infinite set of intervals spread out over time. Each interval defines a window in which recurrence is possible. [Repetition](repetition.md) and [retrying](retrying.md) are two similar concepts in the domain of scheduling. It is the same concept and idea, only one of them looks for successes and the other one looks for failures. When schedules are used to repeat or retry effects, the starting boundary of each interval produced by a schedule is used as the moment when the effect will be executed again. Schedules allow us to define and compose flexible recurrence schedules, which can be used to **repeat** actions, or **retry** actions in the event of errors. We will discuss them on the following pages. A variety of [combinators](combinators.md) exist for transforming and combining schedules, and the companion object for `Schedule` contains [all common types of schedules](built-in-schedules.md), both for performing retrying and repetition. --- ## Repetition In the case of repetition, ZIO has a `ZIO#repeat` function, which takes a schedule as a repetition policy and returns another effect that describes an effect with repetition strategy according to that policy. Repeat policies are used in the following functions: * `ZIO#repeat` — Repeats an effect until the schedule is done. * `ZIO#repeatOrElse` — Repeats an effect until the schedule is done, with a fallback for errors. > _**Note:**_ > > Scheduled recurrences are in addition to the first execution, so that `io.repeat(Schedule.once)` yields an effect that executes `io`, and then if that succeeds, executes `io` an additional time. Let's see how we can create a repeated effect by using `ZIO#repeat` function: ```scala val action: ZIO[R, E, A] = ??? val policy: Schedule[R1, A, B] = ??? val repeated = action repeat policy ``` There is another version of `repeat` that helps us to have a fallback strategy in case of errors, if something goes wrong we can handle that by using the `ZIO#repeatOrElse` function, which helps up to add an `orElse` callback that will run in case of repetition failure: ```scala val action: ZIO[R, E, A] = ??? val policy: Schedule[R1, A, B] = ??? val orElse: (E, Option[B]) => ZIO[R1, E2, B] = ??? val repeated = action repeatOrElse (policy, orElse) ``` --- ## Retrying(Schedule) In the case of retrying, ZIO has a `ZIO#retry` function, which takes a schedule as a repetition policy and returns another effect that describes an effect with repetition strategy which will retry following the failure of the original effect. Repeat policies are used in the following functions: * `ZIO#retry` – Retries an effect until it succeeds. * `ZIO#retryOrElse` — Retries an effect until it succeeds, with a fallback for errors. Let's see how we can create a repeated effect by using `ZIO#retry` function: ```scala val action: ZIO[R, E, A] = ??? val policy: Schedule[R1, E, S] = ??? val repeated = action retry policy ``` There is another version of `retry` that helps us to have a fallback strategy in case of erros, if something goes wrong we can handle that by using the `ZIO#retryOrElse` function, which helps up to add an `orElse` callback that will run in case of failure of repetition failure: ```scala val action: ZIO[R, E, A] = ??? val policy: Schedule[R1, A, B] = ??? val orElse: (E, S) => ZIO[R1, E1, A1] = ??? val repeated = action retryOrElse (policy, orElse) ``` --- ## Accessor Methods (deprecated) Accessor methods are little helper methods that lookup a service from the environment, and then forward your call to that service. :::info The [service pattern](service-pattern.md) provides a better way to structure programs, and it does not need accessor methods. Therefore, accessor methods are now deprecated. ::: ## What is an Accessor Method? Imagine a service defined as: ```scala trait BlobStorage { def get(id: String): ZIO[Any, Throwable, Array[Byte]] def put(content: Array[Byte]): ZIO[Any, Throwable, String] } ``` The accessor methods are then defined as: ```scala object BlobStorage { // Accessor method for BlobStorage.get def get(id: String): ZIO[BlobStorage, Throwable, Array[Byte]] = ZIO.serviceWithZIO[BlobStorage](_.get(id)) // Accessor method for BlobStorage.put def put(content: Array[Byte]): ZIO[BlobStorage, Throwable, String] = ZIO.serviceWithZIO[BlobStorage](_.put(content)) } ``` Each accessor method fetches the service from the environment, and then immediately forwards the method call. The service can now be used as: ```scala BlobStorage.get("blob-id") // returns a ZIO[BlobStorage, Throwable, Array[Byte]] ``` Notice how the `BlobStorage` trait is in the environment (the `R` channel) of the returned ZIO. ## Why are accessor methods deprecated? Accessor methods have some drawbacks: - You must write more code. - The extra code must stay in sync with the service's trait. - The service is looked up in the environment each time it is used. This incurs (a small) performance penalty. - The ZIO environment permeates deeper into your code than strictly necessary. This problem is exacerbated when services start exposing the services they depend on in the `R` channel of their method's return types. The recommended [service pattern](service-pattern.md) injects service dependencies directly, and therefore has none of these problems. ## Monomorphic Services We can use the `@accessible` macro to generate _service member accessors_: ```scala @accessible trait ServiceA { def method(input: Something): UIO[Unit] } // below will be autogenerated object ServiceA { def method(input: Something) = ZIO.serviceWithZIO[ServiceA](_.method(input)) } ``` For normal values, a `ZIO` with `Nothing` on error channel is generated: ```scala @accessible trait ServiceB { def pureMethod(input: Something): SomethingElse } // below will be autogenerated object ServiceB { def pureMethod(input: Something): ZIO[ServiceB, Nothing, SomethingElse] = ZIO.serviceWith[ServiceB](_.pureMethod(input)) } ``` The `@throwing` annotation will mark impure methods. Using this annotation will request ZIO to push the error on the error channel: ```scala @accessible trait ServiceC { @throwing def impureMethod(input: Something): SomethingElse } // below will be autogenerated object ServiceC { def impureMethod(input: Something): ZIO[ServiceC, Throwable, SomethingElse] = ZIO.serviceWithZIO[ServiceC](s => ZIO(s.impureMethod(input))) } ``` Below is a fully working example: ```scala @accessible trait KeyValueStore { def set(key: String, value: Int): Task[Int] def get(key: String): Task[Int] } case class InmemoryKeyValueStore(map: Ref[Map[String, Int]]) extends KeyValueStore { override def set(key: String, value: Int): Task[Int] = map.update(_.updated(key, value)).map(_ => value) override def get(key: String): Task[Int] = map.get.map(_.get(key)).someOrFailException } object InmemoryKeyValueStore { val layer: ULayer[KeyValueStore] = ZLayer { for { map <- Ref.make(Map[String, Int]()) } yield InmemoryKeyValueStore(map) } } object MainApp extends ZIOAppDefault { val myApp = for { _ <- KeyValueStore.set("key", 5) key <- KeyValueStore.get("key") } yield key def run = myApp.provide(InmemoryKeyValueStore.layer).debug } ``` ## Writing Polymorphic Services ### With Proper Type Parameters If the service is polymorphic for some proper types, we can use the `@accessible` macro like previous examples. Assume we have a `KeyValueStore` like below, as we will see using `@accessible` will generate us the accessor methods: ```scala @accessible trait KeyValueStore[K, V] { def set(key: K, value: V): Task[V] def get(key: K): Task[V] } case class InmemoryKeyValueStore(map: Ref[Map[String, Int]]) extends KeyValueStore[String, Int] { override def set(key: String, value: Int): Task[Int] = map.update(_.updated(key, value)).map(_ => value) override def get(key: String): Task[Int] = map.get.map(_.get(key)).someOrFailException } object InmemoryKeyValueStore { val layer: ULayer[KeyValueStore[String, Int]] = ZLayer { for { map <- Ref.make(Map[String, Int]()) } yield InmemoryKeyValueStore(map) } } object MainApp extends ZIOAppDefault { val myApp = for { _ <- KeyValueStore.set("key", 5) key <- KeyValueStore.get[String, Int]("key") } yield key def run = myApp.provide(InmemoryKeyValueStore.layer).debug } ``` ### With Higher-Kinded Type Parameters (`F[_]`) If a service has a higher-kinded type parameter like `F[_]` we should use the `accessibleM` macro. Here is an example of such a service: ```scala @accessibleM[Task] trait KeyValueStore[K, V, F[_]] { def set(key: K, value: V): F[V] def get(key: K): F[V] } case class InmemoryKeyValueStore(map: Ref[Map[String, Int]]) extends KeyValueStore[String, Int, Task] { override def set(key: String, value: Int): Task[Int] = map.update(_.updated(key, value)).map(_ => value) override def get(key: String): Task[Int] = map.get.map(_.get(key)).someOrFailException } object InmemoryKeyValueStore { val layer: ULayer[KeyValueStore[String, Int, Task]] = ZLayer { for { map <- Ref.make(Map[String, Int]()) } yield InmemoryKeyValueStore(map) } } object MainApp extends ZIOAppDefault { val myApp = for { key <- KeyValueStore.set[String, Int]("key", 5) _ <- KeyValueStore.get[String, Int]("key") } yield key def run = myApp.provide(InmemoryKeyValueStore.layer).debug } ``` ### With Higher-Kinded Type Parameters (`F[_, _]`) If the service has a higher-kinded type parameter like `F[_, _]` we should use the `accessibleMM` macro. Let's see an example: ```scala @accessibleMM[IO] trait KeyValueStore[K, V, E, F[_, _]] { def set(key: K, value: V): F[E, V] def get(key: K): F[E, V] } case class InmemoryKeyValueStore(map: Ref[Map[String, Int]]) extends KeyValueStore[String, Int, String, IO] { override def set(key: String, value: Int): IO[String, Int] = map.update(_.updated(key, value)).map(_ => value) override def get(key: String): IO[String, Int] = map.get.map(_.get(key)).someOrFail(s"key not found: $key") } object InmemoryKeyValueStore { val layer: ULayer[KeyValueStore[String, Int, String, IO]] = ZLayer { for { map <- Ref.make(Map[String, Int]()) } yield InmemoryKeyValueStore(map) } } object MainApp extends ZIOAppDefault { val myApp = for { _ <- KeyValueStore.set[String, Int, String]("key", 5) key <- KeyValueStore.get[String, Int, String]("key") } yield key def run = myApp.provide(InmemoryKeyValueStore.layer).debug } ``` --- ## Defining Polymorphic Services in ZIO As we discussed [here](../contextual/zenvironment.md), the `ZEnvironment`, which is the underlying data type used by `ZLayer`, is backed by a type-level mapping from types of services to implementations of those services. This functionality is backed by `izumi.reflect.Tag`, which captures a type as a value. We just need to know what is the type of service when we put it in the `ZEnvironment` because `ZEnvironment` is essentially a map from _service types (interfaces)_ to _implementation of those interfaces_. To implement the map, the `ZEnvironment` needs a type tag for the new service, and also needs a way to remove the old service from the type level map. So we should have service type information at the runtime. We can think of `Tag[A]` as like a `TypeTag[A]` or `ClassTag[A]` from the Scala standard library but available on a cross-version and cross-platform basis. Basically, it carries information about a certain type into runtime that was available at compile time. Methods that construct `ZEnvironment` values generally require a tag for the value being included in the “bundle of services”. As a user, we should not normally interact with `Tag` except where we define polymorphic services. In general, a `Tag` should always be available whenever we have a concrete type. The only time we should have to use it is when we have a _polymorphic service_. If we are using polymorphic code, we need to provide implicit evidence that a tag exists for that type (`implicit tag: Tag[A]`) or as a context-bound for that type parameter: (`A: Tag`). Let's try to write a polymorphic service. Assume we have the following service interface: ```scala trait KeyValueStore[K, V, E, F[_, _]] { def get(key: K): F[E, V] def set(key: K, value: V): F[E, V] def remove(key: K): F[E, Unit] } ``` In the next step, we are going to write its accessors (note: [accessor methods](accessor-methods.md) are deprecated). We might end up with the following snippet code: ```scala object KeyValueStore { def get[K, V, E](key: K): ZIO[KeyValueStore[K, V, E, IO], E, V] = ZIO.serviceWithZIO[KeyValueStore[K, V, E, IO]](_.get(key)) def set[K, V, E](key: K, value: V): ZIO[KeyValueStore[K, V, E, IO], E, V] = ZIO.serviceWithZIO[KeyValueStore[K, V, E, IO]](_.set(key, value)) def remove[K, V, E](key: K): ZIO[KeyValueStore[K, V, E, IO], E, Unit] = ZIO.serviceWithZIO(_.remove(key)) } // error: could not find implicit value for izumi.reflect.Tag[K]. Did you forget to put on a Tag, TagK or TagKK context bound on one of the parameters in K? e.g. def x[T: Tag, F[_]: TagK] = ... // // // : // deriving Tag for K, dealiased: K: // could not find implicit value for Tag[K]: K is a type parameter without an implicit Tag! // ZIO.serviceWithZIO[KeyValueStore[K, V, E, IO]](_.get(key)) // ^ // error: could not find implicit value for izumi.reflect.Tag[K]. Did you forget to put on a Tag, TagK or TagKK context bound on one of the parameters in K? e.g. def x[T: Tag, F[_]: TagK] = ... // // // : // deriving Tag for K, dealiased: K: // could not find implicit value for Tag[K]: K is a type parameter without an implicit Tag! // ZIO.serviceWithZIO[KeyValueStore[K, V, E, IO]](_.set(key, value)) // ^ // error: could not find implicit value for izumi.reflect.Tag[K]. Did you forget to put on a Tag, TagK or TagKK context bound on one of the parameters in K? e.g. def x[T: Tag, F[_]: TagK] = ... // // // : // deriving Tag for K, dealiased: K: // could not find implicit value for Tag[K]: K is a type parameter without an implicit Tag! // ZIO.serviceWithZIO(_.remove(key)) // ^ ``` The compiler generates the following errors: ``` could not find implicit value for izumi.reflect.Tag[K]. Did you forget to put on a Tag, TagK or TagKK context bound on one of the parameters in K? e.g. def x[T: Tag, F[_]: TagK] = ... : deriving Tag for K, dealiased: K: could not find implicit value for Tag[K]: K is a type parameter without an implicit Tag! ZIO.serviceWithZIO[KeyValueStore[K, V, E, IO]](_.get(key)) ``` As the compiler says, we should put `Tag` as a context-bound for `K`, `V`, and `E` type parameters: ```scala object KeyValueStore { def get[K: Tag, V: Tag, E: Tag](key: K): ZIO[KeyValueStore[K, V, E, IO], E, V] = ZIO.serviceWithZIO[KeyValueStore[K, V, E, IO]](_.get(key)) def set[K: Tag, V: Tag, E: Tag](key: K, value: V): ZIO[KeyValueStore[K, V, E, IO], E, V] = ZIO.serviceWithZIO[KeyValueStore[K, V, E, IO]](_.set(key, value)) def remove[K: Tag, V: Tag, E: Tag](key: K): ZIO[KeyValueStore[K, V, E, IO], E, Unit] = ZIO.serviceWithZIO(_.remove(key)) } ``` Now, we can continue and implement the in-memory version of this key-value store: ```scala case class InmemoryKeyValueStore(map: Ref[Map[String, Int]]) extends KeyValueStore[String, Int, String, IO] { override def get(key: String): IO[String, Int] = map.get.map(_.get(key)).someOrFail(s"$key not found") override def set(key: String, value: Int): IO[String, Int] = map.update(_.updated(key, value)).map(_ => value) override def remove(key: String): IO[String, Unit] = map.update(_.removed(key)) } object InmemoryKeyValueStore { def layer: ULayer[KeyValueStore[String, Int, String, IO]] = ZLayer { Ref.make(Map[String, Int]()).map(InmemoryKeyValueStore.apply) } } ``` The last step is to use the service in a ZIO application: ```scala object MainApp extends ZIOAppDefault { val myApp: ZIO[KeyValueStore[String, Int, String, IO], String, Unit] = for { _ <- KeyValueStore.set[String, Int, String]("key1", 3).debug _ <- KeyValueStore.get[String, Int, String]("key1").debug _ <- KeyValueStore.remove[String, Int, String]("key1") _ <- KeyValueStore.get[String, Int, String]("key1").either.debug } yield () def run = myApp.provide(InmemoryKeyValueStore.layer) } // Output: // 3 // 3 // not found ``` Note that in the above example, one might want to write accessors more polymorphic. So in this case we should add `TagKK` as a context-bound of the `F` type parameter: ```scala object KeyValueStore { def get[K: Tag, V: Tag, E: Tag, F[_, _] : TagKK](key: K): ZIO[KeyValueStore[K, V, E, F], Nothing, F[E, V]] = ZIO.serviceWith[KeyValueStore[K, V, E, F]](_.get(key)) def set[K: Tag, V: Tag, E: Tag, F[_, _] : TagKK](key: K, value: V): ZIO[KeyValueStore[K, V, E, F], Nothing, F[E, V]] = ZIO.serviceWith[KeyValueStore[K, V, E, F]](_.set(key, value)) def remove[K: Tag, V: Tag, E: Tag, F[_, _] : TagKK](key: K): ZIO[KeyValueStore[K, V, E, F], E, Unit] = ZIO.serviceWith(_.remove(key)) } ``` --- ## Introduction to Writing ZIO Services Defining services in ZIO is not very different from object-oriented style, it has the same principle: coding to an interface, not an implementation. Therefore, ZIO encourages us to implement this principle by using _Service Pattern_, which is quite similar to the object-oriented style. Before diving into writing services in ZIO style, let's review how we define them in an object-oriented fashion in the next section. ## Defining Services in OOP Here are the steps we take to implement a service in object-oriented programming: 1. **Service Definition**— In object-oriented programming, we define services with traits. A service is a bundle of related functionality that is defined in a trait: ```scala trait FooService { } ``` 2. **Service Implementation**— We implement these services by using classes: ```scala class FooServiceImpl extends FooService { } ``` 3. **Defining Dependencies**— If the creation of a service depends on other services, we can define these dependencies by using constructors: ```scala trait ServiceA { } trait ServiceB { } class FooServiceImpl(a: ServiceA, b: ServiceB) extends FooService { } ``` In object-oriented programming, the best practice is to _program to an interface, not an implementation_. So in the previous example, `ServiceA` and `ServiceB` are interfaces, not concrete classes. 4. **Injecting Dependencies**— Now, the client of `FooServiceImpl` service can provide its own implementation of `ServiceA` and `ServiceB`, and inject them to the `FooServiceImpl` constructor: ```scala class ServiceAImpl extends ServiceA class ServiceBImpl extends ServiceB val fooService = new FooServiceImpl(new ServiceAImpl, new ServiceBImpl) ``` Sometimes, as the number of dependent services grows and the dependency graph of our application becomes complicated, we need an automatic way of wiring and providing dependencies into the services of our application. In these situations, we might use a dependency injection framework to do all its magic machinery for us. ## Defining Services in ZIO A service is a group of functions that deals with only one concern. Keeping the scope of each service limited to a single responsibility improves our ability to understand code, in that we need to focus only on one topic at a time without juggling too many concepts together in our head. In functional Scala as well as in object-oriented programming the best practice is to _Program to an Interface, Not an Implementation_. This is the most important design principle in software development and helps us to write maintainable code by: * Allowing the client to hold an **interface as a contract** and don't worry about the implementation. The interface signature determines all operations that should be done. * Enabling a developer to **write more testable programs**. When we write a test for our business logic we don't have to run and interact with real services like databases which makes our test run very slow. If our code is correct our test code should always pass, there should be no hidden variables or depend on outside sources. We can't know that the database is always running correctly. We don't want to fail our tests because of the failure of external service. * Providing the ability to **write more modular applications**. So we can plug in different implementations for different purposes without a major modification. It is not mandatory, but ZIO encourages us to follow this principle by bundling related functionality as an interface by using the _Service Pattern_. The core idea is that a layer depends upon the interfaces exposed by the layers immediately below itself, but is completely unaware of its dependencies' internal implementations. In object-oriented programming: - **Service Definition** is done by using _interfaces_ (Scala trait or Java Interface). - **Service Implementation** is done by implementing interfaces using _classes_ or creating _new object_ of the interface. - **Defining Dependencies** is done by using _constructors_. They allow us to build classes, given their dependencies. This is called constructor-based dependency injection. We have a similar analogy in the Service Pattern, except instead of using _constructors_ we use **`ZLayer`** to define dependencies. So in ZIO fashion, we can think of `ZLayer` as a service constructor. --- ## Introduction to Reloadable Services Reloadable services are a feature in ZIO that allow us to reload services when necessary. With ZIO When we reload a service, it will automatically deallocate any resources that the service was using. This includes any open files, network connections, or database connections. ZIO will then reallocate new resources for the new service. This process of deallocation and reallocation is handled automatically by ZIO, so you don't need to worry about it. Here are some examples of how we might use reloadable services: - **Config changes**: If we make a change to the configuration of a service, we might want to reload the service so that it can pick up the new configuration. - **Scheduled reloads**: We might want to reload a service on a regular interval, such as every n minutes. This can be useful for services that need to be refreshed on a regular basis. - **Reload on change on Schema**: Assume we have a service that reads data from a database. If we make a change to the database schema, we might want to reload the service so that it can pick up the new schema. This article explores two methods for implementing reloadable services in ZIO: 1. The first method is a non-trivial method that uses the `Reloadable` service, which requires some boilerplate code. 2. The second method is a simpler method introduced by `zio-macros` that uses the `ServiceReloader` service. Before going into further detail, through this article whenever we use `Counter` class, we refere to this source code: ```scala trait Counter { def increment: UIO[Unit] def get: UIO[Int] } object Counter { val increment: ZIO[Counter, Nothing, Unit] = ZIO.serviceWithZIO[Counter](_.increment) val get: ZIO[Counter, Nothing, RuntimeFlags] = ZIO.serviceWithZIO[Counter](_.get) val live: ZLayer[Any, Nothing, Counter] = ZLayer.scoped { for { id <- Ref.make(UUID.randomUUID()) ref <- Ref.make(0) service = CounterLive(id, ref) _ <- service.acquire _ <- ZIO.addFinalizer(service.release) } yield service } } final case class CounterLive(id: Ref[UUID], ref: Ref[Int]) extends Counter { def acquire: UIO[Unit] = { Random.nextUUID .flatMap(n => id.set(n) *> ZIO.debug(s"Acquired counter $n")) } def increment: UIO[Unit] = ref.update(_ + 1) def get: UIO[Int] = ref.get def release: UIO[Unit] = id.get.flatMap(id => ZIO.debug(s"Released counter $id")) } ``` ## 1. The `Reloadable` Service In line with the principles of typical ZIO services, reloadable services are specifically crafted to operate seamlessly within the ZIO environment. The `Reloadable[Service]` data type serves as a wrapper around any reloadable service. This data type encompasses two fundamental methods: `get` and `reload`. The `get` method facilitates the retrieval of the underlying service managed by the `ScopedRef`, while the `reload` method enables the reloading of the service. ### Reloadable Operations Before diving into further details, let's begin by examining the definition of the `Reloadable` class: ```scala case class Reloadable[Service](scopedRef: ScopedRef[Service], reload: IO[Any, Unit]) { def get: UIO[Service] = scopedRef.get def reloadFork: UIO[Unit] = reload.ignoreLogged.forkDaemon.unit } ``` The `Reloadable` service encapsulates a [scoped reference](../resource/scopedref.md) to a service and provides methods to retrieve the service value (`get`) and trigger service reloading (`reload` and `reloadFork`). The two fundamental operations of `Reloadable` are as follows: 1. `Reloadable#get` - By calling the `get` method, we can retrieve the underlying service and interact with it directly. For example, let's consider that we have acquired the `Reloadable[Counter]` service from the ZIO environment using the `ZIO.service[Reloadable[Counter]]` accessor. We can then use the `get` method to obtain the Counter instance and directly perform operations on it: ```scala val app: ZIO[Reloadable[Counter], Nothing, Unit] = for { reloadable <- ZIO.service[Reloadable[Counter]] counter <- reloadable.get } yield () ``` 2. **`Reloadable#reload`**— This operation involves acquiring a new service and releasing the old one, thereby enabling the reloading of the service: ```scala val app: ZIO[Reloadable[Counter], Any, Unit] = for { reloadable <- ZIO.service[Reloadable[Counter]] counter <- reloadable.get _ <- counter.increment _ <- counter.increment _ <- counter.increment _ <- counter.get.debug("Counter value is") _ <- reloadable.reload counter <- reloadable.get _ <- counter.increment _ <- counter.increment _ <- counter.get.debug("Counter value is") } yield () ``` ### Creating Reloadable Services Up to this point, we have explored the process of acquiring reloadable services from the ZIO environment and interacting with them. However, these workflows cannot be executed without fulfilling their requirements. For instance, in the previous example, the type of our workflow is `ZIO[Reloadable[Counter], Any, Unit]`. This implies that we need to provide a layer of type `Reloadable[Counter]`. It is necessary to create reloadable services and `provide` them as a `ZLayer`. In this section, we will delve into the creation of such services. First, let's explore the definition of the two primary constructors for Reloadable: ```scala object Reloadable { def manual[In, E, Out]( layer: ZLayer[In, E, Out] ): ZLayer[In, E, Reloadable[Out]] = ??? def auto[In, E, Out]( layer: ZLayer[In, E, Out], schedule: Schedule[In, Any, Any] ): ZLayer[In, E, Reloadable[Out]] = ??? } ``` There are two fundamental approaches to creating reloadable services: 1. **`Reloadable.manual`** - Using the manual constructor, we can create a reloadable service and subsequently reload it whenever needed by explicitly invoking the `Reloadable#reload` method. This method accepts a layer of type `ZLayer[In, E, Out]` and returns a layer of type `ZLayer[In, E, Reloadable[Out]]`. Continuing the previous example, assume we have written the `Counter.live` as follows: ```scala object Counter { val live: ZLayer[Any, Nothing, Counter] = ZLayer.scoped { for { id <- Ref.make(UUID.randomUUID()) ref <- Ref.make(0) service = CounterLive(id, ref) _ <- service.acquire _ <- ZIO.addFinalizer(service.release) } yield service } } ``` We can easily convert that to reloadable layer using `Reloadable.manual`: ```scala object Counter { val live: ZLayer[Any, Nothing, Counter] = ??? val reloadable: ZLayer[Any, Nothing, Reloadable[Counter]] = Reloadable.manual(live) } ``` Alternatively, we can directly utilize the `ZLayer#reloadableManual` method: ```scala object Counter { val live: ZLayer[Any, Nothing, Counter] = ??? val reloadable: ZLayer[Any, Nothing, Reloadable[Counter]] = live.reloadableManual } ``` Now we can `provide` the `Counter.reloadable` layer to the app workflow and execute the application: ```scala object ReloadableExample extends ZIOAppDefault { val app: ZIO[Reloadable[Counter], Any, Unit] = for { reloadable <- ZIO.service[Reloadable[Counter]] counter <- reloadable.get _ <- counter.increment _ <- counter.increment _ <- counter.increment _ <- counter.get.debug("Counter value is") _ <- reloadable.reload *> ZIO.sleep(1.second) counter <- reloadable.get _ <- counter.increment _ <- counter.increment _ <- counter.get.debug("Counter value is") } yield () def run = app.provide(Counter.reloadable) } ``` This program defines an application that operates on a reloadable counter service (`Reloadable[Counter]`). It obtains the `Counter` service from the reloadable service, performs three `increment` operations, displays its current value, proceeds to `reload` the counter service, performs two additional increment operations, and finally displays its value once again. Therefore, the expected output would be as follows: ```bash Acquired counter d04519a3-7332-43ca-bc86-f61fbaf2e3d6 Counter value: 3 Released counter d04519a3-7332-43ca-bc86-f61fbaf2e3d6 Acquired counter bc66ba00-0b50-4e6e-9f60-c38b6e140a82 Counter value: 2 Released counter bc66ba00-0b50-4e6e-9f60-c38b6e140a82 ``` Observing the behavior, we notice that the service undergoes reloading, causing the counter to reset and begin incrementing from zero once more. 2. **`Reloadable.auto`** - By utilizing this constructor, we can provide a schedule alongside a layer of type `ZLayer[In, E, Out]`, resulting in a layer of reloadable service that will be automatically reloaded based on the specified schedule. Additionally, there is another constructor called `Reloadable.autoFromConfig` which can be used to extract the schedule from the ZIO environment. Let's change the previous example to reload the Counter service automatically every 5 second. First we need to create auto reloadable service: ```scala object Counter { val live: ZLayer[Any, Nothing, Counter] = ??? val autoReloadable: ZLayer[Any, Nothing, Reloadable[Counter]] = Reloadable.auto(live, Schedule.fixed(5.seconds)) } ``` Or we can use `ZLayer#reloadableAuto` to convert a layer to auto reloadable service: ```scala object Counter { val live: ZLayer[Any, Nothing, Counter] = ??? val autoReloadable: ZLayer[Any, Nothing, Reloadable[Counter]] = live.reloadableAuto(Schedule.fixed(5.seconds)) } ``` Finally, we don't require to manually execute `Reloadable#reload` and the service will be reloaded every 5 second: ```scala object AutoReloadableExample extends ZIOAppDefault { val app: ZIO[Reloadable[Counter], Any, Unit] = for { reloadable <- ZIO.service[Reloadable[Counter]] counter <- reloadable.get _ <- counter.increment _ <- counter.increment _ <- counter.increment _ <- counter.get.debug("Counter value is") _ <- ZIO.sleep(6.second) counter <- reloadable.get // getting reloadable service from environment _ <- counter.increment _ <- counter.increment _ <- counter.get.debug("Counter value is") } yield () def run = app.provide(Counter.autoReloadable) } ``` ## 2. The `ServiceReloader` Service Please note that in the previous example, there was no need for manual service reloading. However, we still had to manually retrieve the reloadable service from the environment using `ZIO.service[Reloadable[Counter]]` and then access the `Counter` service from the reloadable counter service. This approach involves some boilerplate code and indirection. We aim to adopt an approach that eliminates the necessity of `Reloadable[Service]` from the environment and instead directly requires the `Service` from the environment. To address this issue, the `ServiceReloader` has been developed as a solution. It is part of the `zio-macros` package. To utilize it, we need to include the following line in our `build.sbt` file: ```scala libraryDependencies ++= Seq("dev.zio" %% "zio-macros" % "") ``` The `ServiceReloader` serves as a registry for services, enabling dynamic reloading of services. By applying the `reloadable` operator on `ZLayer`, we can create a reloadable version of our service. Subsequently, we can simply invoke `ServiceLoader.reload` to initiate the reloading of the service. Let's explore the definition of the ServiceLoader trait: ```scala trait ServiceReloader { def register[A: Tag: IsReloadable](serviceLayer: ZLayer[Any, Any, A]): IO[ServiceReloader.Error, A] def reload[A: Tag]: IO[ServiceReloader.Error, Unit] } ``` For example, if we require a reloadable `Counter` service, we can simply invoke `ServiceReloader.reload[Counter]`. The resulting type will be `ZIO[ServiceReloader, ServiceReloader.Error, Unit]`. Consequently, instead of providing a ZLayer of type `Reloadable[Counter]`, we now need to provide a layer of type `ServiceReloader`. With this approach, there is no longer a need to retrieve `Reloadable[Counter]` from the ZIO environment, eliminating the requirement to access `Counter` from an instance of the `Reloadable[Counter]` class. Instead, we can work with services in a manner consistent with the idiomatic approach used for regular services. Thus, rather than calling `ZIO.serviceWithZIO[Reloadable[Counter]](_.get)`, we can conveniently use `ZIO.service[Counter]` to obtain the `Counter` service directly from the ZIO environment. Let's see how we can rewrite the `Reloadable.manual` example with this approach: ```scala object ServiceReloaderExample extends ZIOAppDefault { def app: ZIO[Counter with ServiceReloader, ServiceReloader.Error, Unit] = for { _ <- Counter.increment _ <- Counter.increment _ <- Counter.increment _ <- Counter.get.debug("Counter value") _ <- ServiceReloader.reload[Counter] _ <- ZIO.sleep(1.seconds) _ <- Counter.increment _ <- Counter.increment _ <- Counter.get.debug("Counter value") } yield () def run = app.provide(Counter.reloadable, ServiceReloader.live) } ``` To create a reloadable layer, we need to import `zio.macros._`. Subsequently, by invoking the `ZLayer#reloadable` method, we can transform the `live` layer into a layer that depends on `ServiceReloader` and provides `Counter` services: ```scala object Counter { val live: ZLayer[Any, Nothing, Counter] = ??? val reloadable: ZLayer[ServiceReloader, ServiceReloader.Error, Counter] = live.reloadable } ``` We can further enhance this application by decoupling the reload process from the application logic. In doing so, each time the service is reloaded, subsequent calls to the service will be served with the freshly reloaded services: ```scala object ServiceReloaderParallelWorkflowExample extends ZIOAppDefault { def reloadWorkflow = ServiceReloader.reload[Counter].delay(5.seconds) def app: ZIO[Counter with ServiceReloader, ServiceReloader.Error, Unit] = for { _ <- Counter.increment _ <- Counter.increment _ <- Counter.increment _ <- Counter.get.debug("Counter value") _ <- ZIO.sleep(6.seconds) _ <- Counter.increment _ <- Counter.increment _ <- Counter.increment _ <- Counter.get.debug("Counter value") } yield () def run = (app <&> reloadWorkflow).provide(Counter.reloadable, ServiceReloader.live) } ``` ## Conclusion Int this article we introduced two methods for implementing reloadable services in ZIO. The first method involves using the `Reloadable` service, which requires some boilerplate code. With this approach, services can be manually reloaded using the reload method. The second method, introduced by `zio-macros`, simplifies the process by utilizing the `ServiceReloader` service. This approach eliminates the need for retrieving the reloadable service from the environment and allows direct access to the service. Overall, reloadable services in ZIO offer a powerful tool for managing services that require reloading, enabling seamless integration within the ZIO environment and simplifying service management in complex applications. All the source code associated with this article is available on the [ZIO Quickstart](https://github.com/zio/zio-quickstarts/tree/master/zio-quickstart-reloadable-services) on Github. --- ## The Four Elements of Service Pattern Writing services in ZIO using the _Service Pattern_ is very similar to the object-oriented way of defining services. We use scala traits to define services, classes to implement services, and constructors to define service dependencies. Finally, we lift the class constructor into the `ZLayer`. Let's start learning this service pattern by writing a `DocRepo` service: ## 1. Service Definition Traits are how we define services. A service could be all the stuff that is related to one concept with singular responsibility. We define the service definition with a trait named `DocRepo`: ```scala final case class Doc( title: String, description: String, language: String, format: String, content: Array[Byte] ) trait DocRepo { def get(id: String): ZIO[Any, Throwable, Doc] def save(document: Doc): ZIO[Any, Throwable, String] def delete(id: String): ZIO[Any, Throwable, Unit] def findByTitle(title: String): ZIO[Any, Throwable, List[Doc]] } ``` ## 2. Service Implementation It is the same as what we did in an object-oriented fashion. We implement the service with the Scala class: ```scala final class DocRepoLive() extends DocRepo { override def get(id: String): ZIO[Any, Throwable, Doc] = ??? override def save(document: Doc): ZIO[Any, Throwable, String] = ??? override def delete(id: String): ZIO[Any, Throwable, Unit] = ??? override def findByTitle(title: String): ZIO[Any, Throwable, List[Doc]] = ??? } ``` ## 3. Service Dependencies We might need `MetadataRepo` and `BlobStorage` services to implement the `DocRepo` service. Here, we put its dependencies into its constructor. All the dependencies are just interfaces, not implementation. Just like what we did in object-oriented style. First, we need to define the interfaces for `MetadataRepo` and `BlobStorage` services: ```scala final case class Metadata( title: String, description: String, language: String, format: String ) trait MetadataRepo { def get(id: String): ZIO[Any, Throwable, Metadata] def put(id: String, metadata: Metadata): ZIO[Any, Throwable, Unit] def delete(id: String): ZIO[Any, Throwable, Unit] def findByTitle(title: String): ZIO[Any, Throwable, Map[String, Metadata]] } trait BlobStorage { def get(id: String): ZIO[Any, Throwable, Array[Byte]] def put(content: Array[Byte]): ZIO[Any, Throwable, String] def delete(id: String): ZIO[Any, Throwable, Unit] } ``` Now, we can implement the `DocRepo` service: ```scala final class DocRepoLive( metadataRepo: MetadataRepo, blobStorage: BlobStorage ) extends DocRepo { override def get(id: String): ZIO[Any, Throwable, Doc] = (metadataRepo.get(id) <&> blobStorage.get(id)).map { case (metadata, content) => Doc( title = metadata.title, description = metadata.description, language = metadata.language, format = metadata.format, content = content ) } override def save(document: Doc): ZIO[Any, Throwable, String] = for { id <- blobStorage.put(document.content) metadata = Metadata( title = document.title, description = document.description, language = document.language, format = document.format ) _ <- metadataRepo.put(id, metadata) } yield id override def delete(id: String): ZIO[Any, Throwable, Unit] = blobStorage.delete(id) &> metadataRepo.delete(id).unit override def findByTitle(title: String): ZIO[Any, Throwable, List[Doc]] = for { metadatas <- metadataRepo.findByTitle(title) content <- ZIO.foreachPar(metadatas) { (id, metadata) => blobStorage .get(id) .map { content => val doc = Doc( title = metadata.title, description = metadata.description, language = metadata.language, format = metadata.format, content = content ) id -> doc } } } yield content.values.toList } ``` ## 4. ZLayer (Constructor) Now, we create a companion object for `DocRepoLive` data type and lift the service implementation into the `ZLayer`: ```scala object DocRepo { /** * The "live" implementation of the `DocRepo` service. */ val live: ZLayer[BlobStorage & MetadataRepo, Nothing, DocRepo] = ZLayer { for { metadataRepo <- ZIO.service[MetadataRepo] blobStorage <- ZIO.service[BlobStorage] } yield new DocRepoLive(metadataRepo, blobStorage) } } ``` And voila! We have implemented the `DocRepo` service using the _Service Pattern_. ## Assembling the application Similarly, we need to implement the `BlobStorage` and `MetadataRepo` services: ```scala object InmemoryBlobStorage { /** * An in-memory implementation of the `BlobStorage` service. */ val layer = ZLayer { ??? } } object InmemoryMetadataRepo { /** * An in-memory implementation of the `MetadataRepo` service. */ val layer = ZLayer { ??? } } ``` This is how ZIO services are created. Let's use the `DocRepo` service in our application. We should provide `DocRepo` layer to be able to run the application: ```scala object MainApp extends ZIOAppDefault { val app = for { docRepo <- ZIO.service[DocRepo] id <- docRepo.save( Doc( "title", "description", "en", "text/plain", "content".getBytes() ) ) doc <- docRepo.get(id) _ <- Console.printLine( s""" |Downloaded the document with $id id: | title: ${doc.title} | description: ${doc.description} | language: ${doc.language} | format: ${doc.format} |""".stripMargin ) _ <- docRepo.delete(id) _ <- Console.printLine(s"Deleted the document with $id id") } yield () def run = app.provide( DocRepo.live, InmemoryBlobStorage.layer, InmemoryMetadataRepo.layer ) } ``` During writing the application, we don't care which implementation version of the `BlobStorage` and `MetadataRepo` services will be injected into our `app`. Later at the end of the day, it will be provided by one of `ZIO#provide*` methods. That's it! Very simple! ZIO encourages us to follow some of the best practices in object-oriented programming. So it doesn't require us to throw away all our object-oriented knowledge. --- ## The Three Laws of ZIO Environment When we are working with the ZIO environment, one question might arise: "When should we use environment and when do we need to use constructors?". Using ZIO environment follows three laws: ## 1. Service Interface (Trait) **When we are defining service interfaces we should _never_ use the environment for dependencies of the service itself.** For example, if the implementation of service `X` depends on service `Y` and `Z` then these should never be reflected in the trait that defines service `X`. It's leaking implementation details. So the following service definition is wrong because the `BlobStorage` and `MetadataRepo` services are dependencies of the `DocRepo` service's implementation, not the `DocRepo` interface itself: ```scala trait DocRepo { def save(document: Doc): ZIO[BlobStorage & MetadataRepo, Throwable, String] } ``` ## 2. Service Implementation (Class) **When implementing service interfaces, we should accept all dependencies in the class constructor.** Again, let's see how `DocRepoImpl` accepts `BlobStorage` and `MetadataRepo` dependencies from the class constructor: ```scala case class DocRepoImpl( metadataRepo: MetadataRepo, blobStorage: BlobStorage ) extends DocRepo { override def delete(id: String): ZIO[Any, Throwable, Unit] = for { _ <- blobStorage.delete(id) _ <- metadataRepo.delete(id) } yield () override def get(id: String): ZIO[Any, Throwable, Doc] = ??? override def save(document: Doc): ZIO[Any, Throwable, String] = ??? override def findByTitle(title: String): ZIO[Any, Throwable, List[Doc]] = ??? } object DocRepoImpl { val layer: ZLayer[BlobStorage with MetadataRepo, Nothing, DocRepo] = ZLayer { for { metadataRepo <- ZIO.service[MetadataRepo] blobStorage <- ZIO.service[BlobStorage] } yield DocRepoImpl(metadataRepo, blobStorage) } } ``` So keep in mind, we can't do something like this: ```scala case class DocRepoImpl() extends DocRepo { override def delete(id: String): ZIO[Any, Throwable, Unit] = for { blobStorage <- ZIO.service[BlobStorage] metadataRepo <- ZIO.service[MetadataRepo] _ <- blobStorage.delete(id) _ <- metadataRepo.delete(id) } yield () override def get(id: String): ZIO[Any, Throwable, Doc] = ??? override def save(document: Doc): ZIO[Any, Throwable, String] = ??? override def findByTitle(title: String): ZIO[Any, Throwable, List[Doc]] = ??? } ``` ## 3. Business Logic **Finally, in the business logic we should use the ZIO environment to consume services.** Therefore, in the last example, if we inline all accessor methods whenever we are using services, we are using the ZIO environment: ```scala object MainApp extends ZIOAppDefault { val app = for { id <- ZIO.serviceWithZIO[DocRepo](_.save( Doc( "How to write a ZIO application?", "In this tutorial we will learn how to write a ZIO application.", "en", "text/plain", "content".getBytes() ) ) ) doc <- ZIO.serviceWithZIO[DocRepo](_.get(id)) _ <- Console.printLine( s""" |Downloaded the document with $id id: | title: ${doc.title} | description: ${doc.description} | language: ${doc.language} | format: ${doc.format} |""".stripMargin ) _ <- ZIO.serviceWithZIO[DocRepo](_.delete(id)) _ <- Console.printLine(s"Deleted the document with $id id") } yield () def run = app.provide( DocRepoImpl.layer, InmemoryBlobStorage.layer, InmemoryMetadataRepo.layer ) } ``` That's it! These are the most important rules we need to know about the ZIO environment. ---------------- :::info The remaining part of this section can be skipped if you are not an advanced ZIO user. ::: Now let's elaborate more on the first rule. On rare occasions, all of which involve local context that is independent of implementation, it's _acceptable_ to use the environment in the definition of a service. Here are two examples: 1. In a web application, a service may be defined only to operate in the context of an HTTP request. In such a case, the request itself could be stored in the environment: `ZIO[HttpRequest, ...]`. This is acceptable because this use of the environment is part of the semantics of the trait itself, rather than leaking an implementation detail of some particular class that implements the service trait: ```scala type HttpApp = ZIO[HttpRequest, Throwable, HttpResponse] type HttpRoute = Map[String, HttpApp] case class HttpRequest(method: Int, uri: URI, headers: Map[String, String], body: UStream[Byte]) case class HttpResponse(status: Int, headers: Map[String, String], body: UStream[Byte]) object HttpResponse { def apply(status: Int, message: String): HttpResponse = HttpResponse( status = status, headers = Map.empty, body = ZStream.fromChunk( Chunk.fromArray(message.getBytes(StandardCharsets.UTF_8)) ) ) def ok(msg: String): HttpResponse = HttpResponse(200, msg) def error(msg: String): HttpResponse = HttpResponse(500, msg) } trait HttpServer { def serve(map: HttpRoute, host: String, port: Int): ZIO[Any, Throwable, Unit] } object HttpServer { def serve(map: HttpRoute, host: String, port: Int): ZIO[HttpServer, Throwable, Unit] = ZIO.serviceWithZIO(_.serve(map, host, port)) } case class HttpServerLive() extends HttpServer { override def serve(map: HttpRoute, host: String, port: Int): ZIO[Any, Throwable, Unit] = ??? } object HttpServerLive { val layer: URLayer[Any, HttpServer] = ZLayer.succeed(HttpServerLive()) } object MainWebApp extends ZIOAppDefault { val myApp: ZIO[HttpServer, Throwable, Unit] = for { _ <- ZIO.unit healthcheck: HttpApp = ZIO.service[HttpRequest].map { _ => HttpResponse.ok("up") } pingpong = ZIO.service[HttpRequest].flatMap { req => ZIO.ifZIO( req.body.via(ZPipeline.utf8Decode).runHead.map(_.contains("ping")) )( onTrue = ZIO.attempt(HttpResponse.ok("pong")), onFalse = ZIO.attempt(HttpResponse.error("bad request")) ) } map = Map( "/healthcheck" -> healthcheck, "/pingpong" -> pingpong ) _ <- HttpServer.serve(map, "localhost", 8080) } yield () def run = myApp.provideLayer(HttpServerLive.layer) } ``` 2. In a database application, a service may be defined only to operate in the context of a larger database transaction. In such a case, the transaction could be stored in the environment: `ZIO[DatabaseTransaction, ...]`. As in the previous example, because this is part of the semantics of the trait itself (whose functionality all operates within a transaction), this is not leaking implementation details, and therefore it is valid: ```scala trait DatabaseTransaction { def get(key: String): Task[Int] def put(key: String, value: Int): Task[Unit] } object DatabaseTransaction { def get(key: String): ZIO[DatabaseTransaction, Throwable, Int] = ZIO.serviceWithZIO(_.get(key)) def put(key: String, value: Int): ZIO[DatabaseTransaction, Throwable, Unit] = ZIO.serviceWithZIO(_.put(key, value)) } trait Database { def atomically[E, A](zio: ZIO[DatabaseTransaction, E, A]): ZIO[Any, E, A] } object Database { def atomically[E, A](zio: ZIO[DatabaseTransaction, E, A]): ZIO[Database, E, A] = ZIO.serviceWithZIO(_.atomically(zio)) } case class DatabaseLive() extends Database { override def atomically[E, A](zio: ZIO[DatabaseTransaction, E, A]): ZIO[Any, E, A] = ??? } object DatabaseLive { val layer = ZLayer.succeed(DatabaseLive()) } object MainDatabaseApp extends ZIOAppDefault { val myApp: ZIO[Database, Throwable, Unit] = for { _ <- Database.atomically(DatabaseTransaction.put("counter", 0)) _ <- ZIO.foreachPar(List(1 to 10)) { _ => Database.atomically( for { value <- DatabaseTransaction.get("counter") _ <- DatabaseTransaction.put("counter", value + 1) } yield () ) } } yield () def run = myApp.provideLayer(DatabaseLive.layer) } ``` So while it's better to err on the side of "don't put things into the environment of service interface", there are cases where it's acceptable. --- ## Clock Clock service contains some functionality related to time and scheduling. To get the current time in a specific time unit, the `currentTime` function takes a unit as `TimeUnit` and returns `UIO[Long]`: ```scala compile-only val inMilliseconds: UIO[Long] = Clock.currentTime(TimeUnit.MILLISECONDS) val inDays : UIO[Long] = Clock.currentTime(TimeUnit.DAYS) ``` To get current date time in the current timezone the `currentDateTime` function returns a ZIO effect containing `OffsetDateTime`. Also, the Clock service has a very useful functionality for sleeping and creating a delay between jobs. The `sleep` takes a `Duration` and sleeps for the specified duration. It is analogous to `java.lang.Thread.sleep` function, but it doesn't block any underlying thread. It's completely non-blocking. In the following example we are going to print the current time periodically by placing a one second `sleep` between each print call: ```scala compile-only def printTimeForever: ZIO[Any, Throwable, Nothing] = Clock.currentDateTime.flatMap(Console.printLine(_)) *> ZIO.sleep(1.seconds) *> printTimeForever ``` For scheduling purposes like retry and repeats, ZIO has a great data type called [Schedule](../schedule/index.md). --- ## Console The Console service contains simple I/O operations for reading/writing strings from/to the standard input, output, and error console. | Function | Input Type | Output Type | |------------------|-------------------|---------------------------------| | `print` | `line: => String` | `ZIO[Any, IOException, Unit]` | | `printError` | `line: => String` | `ZIO[Any, IOException, Unit]` | | `printLine` | `line: => String` | `ZIO[Any, IOException, Unit]` | | `printLineError` | `line: => String` | `ZIO[Any, IOException, Unit]` | | `readLine` | | `ZIO[Any, IOException, String]` | All functions of the Console service are effectful, this means they are just descriptions of reading/writing from/to the console. As ZIO data type supports monadic operations, we can compose these functions with for-comprehension which helps us to write our program pretty much like an imperative program: ```scala object MyHelloApp extends ZIOAppDefault { val program: ZIO[Any, IOException, Unit] = for { _ <- printLine("Hello, what is you name?") name <- readLine _ <- printLine(s"Hello $name, welcome to ZIO!") } yield () def run = program } ``` Note again, every line of our `program` are descriptions, not statements. As we can see the type of our `program` is `ZIO[Any, IOException, Unit]`, it means to run `program` we do not need any environment, it may fail due to failure of `readLine` and it will produce `Unit` value. --- ## Introduction to ZIO's Built-in Services ZIO already provides four built-in services: 1. **[Console](console.md)** — Operations for reading/writing strings from/to the standard input, output, and error console. 2. **[Clock](clock.md)** — Contains some functionality related to time and scheduling. 3. **[Random](random.md)** — Provides utilities to generate random numbers. 4. **[System](system.md)** — Contains several useful functions related to system environments and properties. When we use these services we don't need to provide their corresponding environment explicitly. ZIO provides built-in live version of ZIO services to our effects, so we do not need to provide them manually. ```scala object MainApp extends ZIOAppDefault { val myApp: ZIO[Any, IOException, Unit] = for { date <- Clock.currentDateTime _ <- ZIO.logInfo(s"Application started at $date") _ <- Console.print("Enter your name: ") name <- Console.readLine _ <- Console.printLine(s"Hello, $name!") } yield () def run = myApp } ``` --- ## Random Random service provides utilities to generate random numbers. It's a functional wrapper of `scala.util.Random`. This service contains various different pseudo-random generators like `nextInt`, `nextBoolean` and `nextDouble`. Each random number generator functions return a `URIO[Random, T]` value. ```scala for { randomInt <- Random.nextInt _ <- Console.printLine(s"A random Int: $randomInt") randomChar <- Random.nextPrintableChar _ <- Console.printLine(s"A random Char: $randomChar") randomDouble <- Random.nextDoubleBetween(1.0, 5.0) _ <- Console.printLine(s"A random double between 1.0 and 5.0: $randomDouble") } yield () ``` Random service has a `setSeed` which helps us to alter the state of the random generator. It is useful for setting up a test version of Random service when we need to reproduce always the same sequence of numbers. ```scala for { _ <- Random.setSeed(0) nextInts <- (Random.nextInt zip Random.nextInt) } yield assert(nextInts == (-1155484576,-723955400)) ``` Also, it has a utility to shuffle a list and to generate random samples from Gaussian distribution: * **shuffle** - Takes a list as an input and shuffles it. * **nextGaussian** — Returns the next pseudorandom, Gaussian ("normally") distributed double value with mean 0.0 and standard deviation 1.0. > **Note**: > > Random numbers that are generated via Random service are not cryptographically strong. Therefore it's not safe to use the ZIO Random service for security domains where a high level of security and randomness is required, such as password generation. --- ## System System service contains several useful functions related to system environments and properties. Both of **system environments** and **system properties** are key/value pairs. They are used to pass user-defined information to our application. Environment variables are global operating system level variables available to all applications running on the same machine, while properties are application-level variables provided to our application. ## System Environment The `env` function retrieves the value of an environment variable: ```scala for { user <- System.env("USER") _ <- user match { case Some(value) => Console.printLine(s"The USER env is: $value") case None => Console.printLine("Oops! The USER env is not set") } } yield () ``` ## System Property Also, the System service has a `property` function to retrieve the value of a system property: ```scala for { user <- System.property("LOG_LEVEL") _ <- user match { case Some(value) => Console.printLine(s"The LOG_LEVEL property is: $value") case None => Console.printLine("Oops! The LOG_LEVEL property is not set") } } yield () ``` ## Miscellaneous With the `lineSeparator` method, we can determine the line separator for the underlying platform: ```scala System.lineSeparator // res2: String = """ // """ ``` --- ## Fiber-local State Both the `FiberRef` and `ZState` data types are state management tools that are scoped to a certain fiber. Their values are only accessible within the fiber that runs them. We have a separate page for the [`FiberRef`](fiberref.md) and [`ZState`](zstate.md) data types which explain how to use them. --- ## FiberRef: Introduction to Fiber-local Storage `FiberRef` is a data structure for managing and accessing thread-local values within a ZIO fiber. Thread-local storage (TLS) is a mechanism that provides each fiber its own separate storage space. A `FiberRef[A]` is a specialized type of mutable reference (`Ref[A]`) that allows us to store and retrieve values of type `A` that are local to a specific fiber. The `FiberRef` data structure allows us to perform operations such as reading the current value, updating the value, or modifying the value atomically within a fiber. It ensures thread-safety and isolation between different fibers, allowing them to have their own independent values for the `FiberRef`. Each fiber maintains its own copy of the fiber-specific variable, and modifications to the variable made by one fiber do not affect the values seen by other fibers. By using `FiberRef`, we can maintain per-fiber context or state information, which can be useful in various scenarios such as managing resources, tracking application-specific information, or carrying contextual data throughout the execution of a fiber. We can think of `FiberRef` as Java's `ThreadLocal` on steroids. So, just like we have `ThreadLocal` in Java we have `FiberRef` in ZIO. So as different threads have different `ThreadLocal`s, we can say different fibers have different `FiberRef`s. They don't intersect or overlap in any way. `FiberRef` is the fiber version of `ThreadLocal` with significant improvements in its semantics. A `ThreadLocal` only has a mutable state in which each thread accesses its own copy, but threads don't propagate their state to their children's. As opposed to `Ref[A]`, the value of a `FiberRef[A]` is bound to an executing fiber. Different fibers who hold the same `FiberRef[A]` can independently set and retrieve values of the reference, without collisions. ```scala for { fiberRef <- FiberRef.make[Int](0) _ <- fiberRef.set(10) v <- fiberRef.get } yield v == 10 ``` ## Motivation Whenever we have some kind of scoped information or context, and we don't want to use the ZIO environment to store it, we can use `FiberRef` to store it. To illustrate this, let's try to find a solution to the _Structured Logging_ problem. In structured logging, we tend to attach contextual information to log messages, such as user id, correlation id, log level, and so on. So assume we have written the following code: ```scala for { _ <- Logging.log("Hello World!") _ <- ZIO.foreachParDiscard(List("Jane", "John")) { name => Logging.logAnnotate("name", name) { for { _ <- Logging.log(s"Received request") fiberId <- ZIO.fiberId.map(_.ids.head) _ <- Logging.logAnnotate("fiber_id", s"$fiberId")( Logging.log("Processing request") ) _ <- Logging.log("Finished processing request") } yield () } } _ <- Logging.log("All requests processed") } yield () ``` We would like to see the following log output: ```scala Hello World! [name=Jane] Received request [name=John] Received request [name=Jane] [fiber_id=7] Processing request [name=John] [fiber_id=8] Processing request [name=John] Finished processing request [name=Jane] Finished processing request All requests processed ``` In the above code, we have two users, `Jane` and `John`, and we want to handle some operations on each user, concurrently. When we perform concurrent operations, we would like to have a way to associate each concurrent operation with its corresponding user and fiber id. So when we log messages, we have all the information available for a specific event. In order to do this, we need a context-aware logging service. This logging service needs to have a **state** that is a place to store annotations. This state can be accessed and modified **concurrently** by multiple fibers. And the important part is that each fiber should have its own isolated copy of the state, so when a fiber modifies the state, it doesn't clobber the state of other fibers. Until now, we can categorize our requirements into two parts: - We need a mechanism to carry some contextual information, without explicitly passing it around. - We need a mechanism to update the state in an isolated fashion, where each fiber can update the state without affecting the state of other fibers. ## Solution In this section, we will look at two solutions to the problem of structured logging we have mentioned above. The first solution has some limitations and drawbacks, so we will choose the second solution as the final solution. ### Solution 1: ZIO Environment One solution is to use the ZIO environment to store the state. It addresses the first requirement, very well. ZIO environment is a nice place to store the contextual states. And to make the state isolated between fibers, we can reintroduce the new state to the environment instead of updating the environment globally: ```scala // Solution 1: Using the ZIO environment to store the contextual state object Logging { type Annotation = Map[String, String] def logAnnotate[R, E, A](key: String, value: String)( zio: ZIO[R with Annotation, E, A] ): ZIO[R with Annotation, E, A] = { for { s <- ZIO.service[Annotation] r <- zio.provideSomeLayer[R](ZLayer.succeed(s.updated(key, value))) } yield (r) } def log(message: String): ZIO[Annotation, Nothing, Unit] = { ZIO.service[Annotation].flatMap { case annotation if annotation.isEmpty => Console.printLine(message).orDie case annotation => val line = s"${annotation.map { case (k, v) => s"[$k=$v]" }.mkString(" ")} $message" Console.printLine(line).orDie } } } ``` The ZIO environment solution provides an explicit method for ensuring type-safety when dealing with contextual data types. However, this increased type-safety may limit flexibility in certain scenarios. For instance, consider a situation where our workflows require multiple cross-cutting services such as `Logging`, `Config`, and `Metrics`. In this case, every instance of application logic would involve a workflow with a type signature like `ZIO[Logging & Config & Metrics & ..., IOException, Any]`. This extensive type declaration restricts easy code refactoring and maintenance, while also distracts our attention from the core business logic. Any modification to the contextual data type necessitates modifying the entire program. Although we successfully utilized the ZIO environment in the previous example to store the state, it is not considered the idiomatic solution. It is preferable to avoid explicitly exposing the type of the state, in this case `Annotation`, within the environment. Anyway, despite these limitations, this solution proves especially beneficial when - When the contextual service holds a **crucial role** in the workflow logic - When there is a requirement for ensuring *type-safety on the service type** within the ZIO environment - When there is no sensible **default value** for such services ### Solution 2: FiberRef The other solution is to use `FiberRef`. FiberRef is a nice way to store the contextual states and make them isolated. Any state maintained by a `FiberRef` will be isolated between fibers. Also, a nice thing about `FiberRef` is that we do not require to place the state in the environment. Let's see how to use `FiberRef` to implement the logging service: ```scala // Solution 2: Using the FiberRef to store the contextual state trait Logger { def logAnnotate[R, E, A](key: String, value: String)( zio: ZIO[R, E, A] ): ZIO[R, E, A] def log(message: String): UIO[Unit] } object Logging extends Logger { def logAnnotate[R, E, A](key: String, value: String)( zio: ZIO[R, E, A] ): ZIO[R, E, A] = currentAnnotations.locallyWith(_.updated(key, value))(zio) def log(message: String): UIO[Unit] = { currentAnnotations.get.flatMap { case annotation if annotation.isEmpty => Console.printLine(message).orDie case annotation => val line = s"${annotation.map { case (k, v) => s"[$k=$v]" }.mkString(" ")} $message" Console.printLine(line).orDie } } val currentAnnotations: FiberRef[Map[String, String]] = Unsafe.unsafe { implicit unsafe => FiberRef.unsafe.make(Map.empty[String, String]) } } ``` Now we can write a program that logs some information: ```scala object FiberRefLoggingExample extends ZIOAppDefault { def run = for { _ <- Logging.log("Hello World!") _ <- ZIO.foreachParDiscard(List("Jane", "John")) { name => Logging.logAnnotate("name", name) { for { _ <- Logging.log(s"Received request") fiberId <- ZIO.fiberId.map(_.ids.head) _ <- Logging.logAnnotate("fiber_id", s"$fiberId")( Logging.log("Processing request") ) _ <- Logging.log("Finished processing request") } yield () } } _ <- Logging.log("All requests processed") } yield () } ``` The output: ```scala Hello World! [name=Jane] Received request [name=John] Received request [name=Jane] [fiber_id=5] Processing request [name=John] [fiber_id=6] Processing request [name=John] Finished processing request [name=Jane] Finished processing request All requests processed ``` > **Note:** > > In the above solution, if we replace the `FiberRef` with `Ref`, the program will not work properly, because the `Ref` is not isolated. The `Ref` will be shared between all fibers, so each fiber clobbers the other fibers' state. To take it a step further, let's modify the previous example to allow the user to change the underlying logging service: ```scala trait Logger { def logAnnotate[R, E, A](key: String, value: String)( zio: ZIO[R, E, A] ): ZIO[R, E, A] def log(message: String): UIO[Unit] } ``` ```scala object Logging { val defaultLogger: Logger = new Logger { def logAnnotate[R, E, A](key: String, value: String)( zio: ZIO[R, E, A] ): ZIO[R, E, A] = currentAnnotations.locallyWith(_.updated(key, value))(zio) def log(message: String): UIO[Unit] = { currentAnnotations.get.flatMap { case annotation if annotation.isEmpty => Console.printLine(message).orDie case annotation => val line = s"${annotation.map { case (k, v) => s"[$k=$v]" }.mkString(" ")} $message" Console.printLine(line).orDie } } } val silentLogger: Logger = new Logger { def logAnnotate[R, E, A](key: String, value: String)( zio: ZIO[R, E, A] ): ZIO[R, E, A] = currentAnnotations.locallyWith(_.updated(key, value))(zio) def log(message: String): UIO[Unit] = ZIO.unit } def log(message: String): ZIO[Any, Nothing, Unit] = currentLogger.get.flatMap(_.log(message)) def logAnnotate[R, E, A](key: String, value: String)( zio: ZIO[R, E, A] ): ZIO[R, E, A] = currentLogger.get.flatMap(_.logAnnotate(key, value)(zio)) def locallyWithLogger[R, E, A](newLogger: Logger)(zio: ZIO[R, E, A]) = { currentLogger.locallyWith(_ => newLogger)(zio) } def updateLogger(logger: Logger => Logger): UIO[Unit] = currentLogger.update(logger) val currentLogger: FiberRef[Logger] = Unsafe.unsafe { implicit unsafe => FiberRef.unsafe.make(defaultLogger) } val currentAnnotations: FiberRef[Map[String, String]] = Unsafe.unsafe { implicit unsafe => FiberRef.unsafe.make(Map.empty[String, String]) } } ``` Now, changing the default logger is made easy with the Logging.withLogger function. Let's disable the default logger for a specific section of our example by utilizing Logging.silentLogger: ```scala object FiberRefChangeDefaultLoggerExample extends ZIOAppDefault { def run = for { _ <- Logging.log("Hello World!") _ <- ZIO.foreachParDiscard(List("Jane", "John")) { name => Logging.locallyWithLogger(Logging.silentLogger) { Logging.logAnnotate("name", name) { for { _ <- Logging.log(s"Received request") fiberId <- ZIO.fiberId.map(_.ids.head) _ <- Logging.logAnnotate("fiber_id", s"$fiberId")( Logging.log("Processing request") ) _ <- Logging.log("Finished processing request") } yield () } } } _ <- Logging.log("All requests processed") } yield () } ``` The output is: ```scala Hello World! All requests processed ``` The solution provided by `FiberRef` offers an **implicit** method to store and propagate contextual data or service in an untyped manner. It helps us reduce redundancy in environment types. For instance, by encoding the `Logging` and `Metrics` services using `FiberRef`, we no longer need to include the `Logging` and `Metrics` service type in the environment type of ZIO workflows. As a result, we can simplify a ZIO effect from `ZIO[Logging & Metrics & UserRepo & DocsRepo, IOException, Unit]` to `ZIO[UserRepo & DocsRepo, IOException, Unit]`. This significantly reduces boilerplate code in our workflows, which helps us to focus on maintaining application logic in a flexible manner. Additionally, as demonstrated in the final example, `FiberRef` proves to be a valuable solution when we have a **default value** for a contextual service or data. We can start the application with default values and, whenever needed, locally or globally change the underlying service or data using `FiberRef#locallyWith` and `FiberRef#update`. In summary, this solution is particularly advantageous in the following scenarios: - When **encoding cross-cutting services** without the need to include them everywhere in the ZIO environment. - When requiring **isolated states** for different fibers. - When having a **default value** for contextual service or data. ## Use Cases Whenever we have some kind of scoped information or context, we can think about `FiberRef` as a way to store that information. When developing applications, there are several use cases for `FiberRef`. Let's take a look at some of them: 1. **Resource management**: `FiberRef` can be utilized to manage resources that are specific to a particular fiber. For example, we can use it to store and access connections to a database or network resources. Each fiber can have its own dedicated resource, ensuring isolation and avoiding contention between different fibers. 2. **Configuration Settings**: It can be used to store configuration settings that are specific to a fiber. This allows different fibers to have their own configuration values, enabling fine-grained control and customization. 3. **Avoiding Synchronization**: By using `FiberRef`, we can eliminate the need for synchronization mechanisms, such as locks or atomic operations, when accessing fiber-specific data. Each fiber operates on its own private copy, avoiding contention with other fibers. 4. **Distributed Tracing**— In an architecture, where we have highly concurrent workflows and distributed services, there is a need to trace requests as they propagate through the services. To be able to trace requests, we can use `FiberRef` to design the system to automatically propagate request-scoped information. 5. **Contextual Logging**— In lot of cases, logs are not independent piece of information, but they are part of a larger context. So other than just logging the message, we also need to log some additional information such as the request ID, the user ID, the session id, and so on. So whe we collect these logs, we can correlate them based on a common data point. Instead of explicitly passing these contextual information, we can use `FiberRef`. 6. **Execution Scoped Configuration**— When we write applications, we would like to make them configurable. So we configure the application once and used it throughout the whole components. Not all configurations are global. There are certain kinds of configurations that are not global, or at least we have a default value for them globally, but we need to change them dynamically for certain regions. `FiberRef` is a nice tool to model these kind of configurations. In ZIO we have several use cases for `FiberRef`. Let's discuss some of them: 1. Whenever we use `ZIO.withParallelism`, we can specify the parallelism factor for a region of code. So this information will be stored inside a `FiberRef`, without any need to pass it around all effects explicitly. When we exit the region, the parallelism factor will be restored to the original value: ```scala object MainApp extends ZIOAppDefault { def myJob(name: String) = ZIO.foreachParDiscard(1 to 3)(i => ZIO.debug(s"The $name-$i job started") *> ZIO.sleep(2.second) ) def run = ZIO.withParallelismUnbounded( for { _ <- myJob("foo") _ <- ZIO.debug("------------------") _ <- ZIO.withParallelism(1)(myJob("bar")) _ <- ZIO.debug("------------------") _ <- myJob("baz") } yield () ) } ``` 2. Using `ZIOAspect.annotated` we can annotate the effect with some contextual information, e.g. the `correlation_id`. This information will be stored inside a `FiberRef`, which will be propagated to all fibers that are created from the same parent fiber. Each fiber will have its own set of annotations. When we log inside a fiber, the logging service will use the fiber's specific annotations to create the log message: ```scala object MainApp extends ZIOAppDefault { def handleRequest(request: String) = for { _ <- ZIO.log(s"Received request.") _ <- ZIO.unit // do something with the request _ <- ZIO.log(s"Finished processing request") } yield () def run = for { _ <- ZIO.log("Hello World!") _ <- ZIO.foreachParDiscard(List(("req1", "1"), ("req2", "2"), ("req3", "3"))){ case (req, id) => handleRequest(req) @@ ZIOAspect.annotated("correlation_id", id) } _ <- ZIO.log("Goodbye!") } yield () } ``` Here is the output (extra columns were removed for better readability): ``` message="Hello World!" message="Received request." correlation_id=2 message="Received request." correlation_id=1 message="Received request." correlation_id=3 message="Finished processing request." correlation_id=3 message="Finished processing request." correlation_id=1 message="Finished processing request." correlation_id=2 message="Goodbye!" ``` 3. Log levels are also maintained by using `FiberRef`. They are stored inside a `FiberRef`, and whenever we want, we can change the log level using the `ZIO.logLevel` operator: ```scala for { _ <- ZIO.log("Application started!") _ <- ZIO.logLevel(LogLevel.Trace) { for { _ <- ZIO.log("Entering trace log level region") _ <- ZIO.log("Doing something") _ <- ZIO.log("Leaving trace log level region") } yield () } _ <- ZIO.log("Application ended!") } yield () ``` 4. The same goes for when we access the environment (e.g. `ZIO.service`), or when we provide a layer to a ZIO effect (e.g. `ZIO#provide`). ZIO uses `FiberRef` under the hood to store the environment: ```scala object MainApp extends ZIOAppDefault { private val fooLayer = ZLayer.succeed("foo") private val barLayer = ZLayer.succeed("bar") def run = (for { _ <- ZIO.service[String].debug("context") _ <- ZIO.service[String].debug("context").provide(barLayer) _ <- ZIO.service[String].debug("context") } yield ()).provide(fooLayer) } // Output: // context: foo // context: bar // context: foo ``` There are several other use cases for `FiberRef` in ZIO itself. We just covered some of them to get you some ideas on how they are used in the real world. ## Operations `FiberRef[A]` has an API almost identical to `Ref[A]`. It includes well-known methods such as: - `FiberRef#get`. Returns the current value of the reference. - `FiberRef#set`. Sets the current value of the reference. - `FiberRef#update` / `FiberRef#updateSome` updates the value with the specified function. - `FiberRef#modify`/ `FiberRef#modifySome` modifies the value with the specified function, computing a return value for the operation. You can also use `locally` to scope `FiberRef` value only for a given effect: ```scala for { correlationId <- FiberRef.make[String]("") v1 <- correlationId.locally("my-correlation-id")(correlationId.get) v2 <- correlationId.get } yield v1 == "my-correlation-id" && v2 == "" ``` ## `Ref` vs. `FiberRef` Let's explore the distinction between `Ref` and `FiberRef` through two practical examples: ```scala object RefExample extends ZIOAppDefault { def run = for { ref <- Ref.make(0) left = ref.updateAndGet(_ + 1).debug("left1") *> ref.updateAndGet(_ + 1).debug("left2") right = ref.updateAndGet(_ + 1).debug("right1") *> ref.updateAndGet(_ + 3).debug("right2") _ <- left <&> right } yield () } ``` One potential result of running this program is as follows: ```scala left1: 1 right1: 2 left2: 3 right2: 6 ``` It is apparent that the `ref` is shared between the `left` and `right` fibers. However, when using FiberRef, each fiber has its own separate storage, isolating them from one another: ```scala object FiberRefExample extends ZIOAppDefault { def run = for { ref <- FiberRef.make(0) left = ref.updateAndGet(_ + 1).debug("left1") *> ref.updateAndGet(_ + 1).debug("left2") right = ref.updateAndGet(_ + 1).debug("right1") *> ref.updateAndGet(_ + 3).debug("right2") _ <- left <&> right } yield () } ``` One possible output of this program is: ```scala left1: 1 right1: 1 left2: 2 right2: 4 ``` We can observe that each fiber has its own storage without interfering with the value of another fiber. ## Propagation Let's go back to the `FiberRef`s analog called `ThreadLocal` and see how it works. If we have thread `A` with its `ThreadLocal` and thread `A` creates a new thread, let's call it thread `B`. When thread `A` sends thread `B` the same `ThreadLocal` then what value does thread `B` see inside the `ThreadLocal`? Well, it sees the default value of the `ThreadLocal`. It does not see `A`s value of the `ThreadLocal`. So in other words, `ThreadLocal`s do not propagate their values across the sort of graph of threads so when one thread creates another, the `ThreadLocal` value is not propagated from parent to child. `FiberRef`s improve on that model quite dramatically. Basically, whenever a child's fiber is created from its parent, the `FiberRef` value of parent fiber propagated to its child fiber. ### Copy-on-Fork `FiberRef[A]` has *copy-on-fork* semantics for `ZIO#fork`. This essentially means that a child `Fiber` starts with `FiberRef` values of its parent. When the child sets a new value of `FiberRef`, the change is visible only to the child itself. The parent fiber still has its own value. So if we create a `FiberRef` and set its value to `5`, and we pass this `FiberRef` to a child fiber, it sees the value `5`. If the child fiber modifies the value from `5` to `6`, the parent fiber can't see that change. So the child fiber gets its own copy of the `FiberRef`, and it can modify it locally. Those changes will not affect the parent fiber: ```scala for { fiberRef <- FiberRef.make(5) promise <- Promise.make[Nothing, Int] _ <- fiberRef .updateAndGet(_ => 6) .flatMap(promise.succeed).fork childValue <- promise.await parentValue <- fiberRef.get } yield assert(parentValue == 5 && childValue == 6) ``` ## Merging FiberRefs ZIO does not only support to propagate `FiberRef` values from parents to childs, but also to merge back these values into the current fiber. This section describes multiple variants for doing so. ### join If we `join` a fiber then the value of its `FiberRef` is merged back into the parent fiber. The default strategy for merging back, is **replacement**. This means whenever a forked fiber joined to its parent fiber, the value of its parent will be replaced with the value of its child `FiberRef`: ```scala for { fiberRef <- FiberRef.make(5) child <- fiberRef.set(6).fork _ <- child.join parentValue <- fiberRef.get } yield assert(parentValue == 6) ``` So if we `fork` a fiber and that child fiber modifies a bunch of `FiberRef`s and then later we `join` it, we get those modifications merged back into the parent fiber. So that's the semantic model of ZIO on `join`. Each fiber has its own `FiberRef` and can modify it independently. Therefore, when multiple child fibers `join` their parent, the last child fiber will override the parent's `FiberRef` value, replacing it with its own. As we can see, `child1` is the last fiber, so its value, which is `6`, gets merged back into its parent: ```scala for { fiberRef <- FiberRef.make(5) child1 <- fiberRef.set(6).fork child2 <- fiberRef.set(7).fork _ <- child2.join _ <- child1.join parentValue <- fiberRef.get } yield assert(parentValue == 6) ``` ### join with Custom Merge Furthermore, we have the ability to customize the initialization of a value when a fiber is forked, as well as the method of value combination when merging back the values. To achieve this, you can specify the desired behavior when making the `FiberRef` using `FiberRef#make`: ```scala for { fiberRef <- FiberRef.make(initial = 0, join = math.max) child <- fiberRef.update(_ + 1).fork _ <- fiberRef.update(_ + 2) _ <- child.join value <- fiberRef.get } yield assert(value == 2) ``` In this example, when the child fiber joins its parent, it employs the max function to determine how to merge the values. It compares the child's FiberRef value (1) with the parent's FiberRef value (2) and selects the higher value as the merged result, which in this case is 2. ### await It is important to note that `await` has no such merge behavior. So `await` waits for the child fiber to finish and gives us its value as an `Exit`, without ever merging any `FiberRef` values back into the parent: ```scala for { fiberRef <- FiberRef.make(5) child <- fiberRef.set(6).fork _ <- child.await parentValue <- fiberRef.get } yield assert(parentValue == 5) ``` `join` has higher-level semantics than `await` because it will fail if the child fiber failed, it will interrupt if the child is interrupted, and it will also merge back its value to its parent. ### inheritAll We can inherit the values from all `FiberRef`s from an existing `Fiber` using the `Fiber#inheritAll` method: ```scala for { fiberRef <- FiberRef.make[Int](0) latch <- Promise.make[Nothing, Unit] fiber <- (fiberRef.set(10) *> latch.succeed(())).fork _ <- latch.await _ <- fiber.inheritAll v <- fiberRef.get } yield v == 10 ``` Note that `inheritAll` is automatically called on `join`. However, `join` will wait for merging the **final** values, while `inheritAll` will merge the **current** values and then continue: ```scala val withJoin = for { fiberRef <- FiberRef.make[Int](0) fiber <- (fiberRef.set(10) *> fiberRef.set(20).delay(2.seconds)).fork _ <- fiber.join // wait for fiber's end and copy final result 20 into fiberRef v <- fiberRef.get } yield assert(v == 20) ``` ```scala val withoutJoin = for { fiberRef <- FiberRef.make[Int](0) fiber <- (fiberRef.set(10) *> fiberRef.set(20).delay(2.seconds)).fork _ <- fiber.inheritAll.delay(1.second) // copy intermediate result 10 into fiberRef and continue v <- fiberRef.get } yield assert(v == 10) ``` ## Compositional Updates and Patch Theory In the previous section, we learned the following: 1. Whenever a child fiber merges back into its parent, the value of the child fiber is, by default, replaced with the parent's value. 2. When we have multiple child fibers, and all of them join their parent, the value of the last child to join will prevail, replacing the parent's value. Let's examine these two rules with a simple example: ```scala object Main extends ZIOAppDefault { val retries: FiberRef[Int] = Unsafe.unsafe { implicit unsafe => FiberRef.unsafe.make(3) } def run = for { _ <- ZIO.unit f1 = retries.set(10).debug("set 10").delay(2.seconds) f2 = retries.set(5).debug("set 5") _ <- f1 <&> f2 _ <- retries.get.debug("final retries value") } yield () } ``` The output of this program is: ```scala set 5: () set 10: () final retries value: 10 ``` As we can see from the program's output, when we delayed the `f1` workflow, it became the last child fiber to join its parent. And guess what? Its value of 10 ended up being the winner! Why? Well, it's because the default rule is that the child's value takes over the parent's value during the merge. ### The Problem While developing the program, we might want to add additional configurations, such as `intervals`. In this case, we can easily include another `FiberRef` that holds the `intervals` config: ```scala object Main extends ZIOAppDefault { val retries: FiberRef[Int] = Unsafe.unsafe { implicit unsafe => FiberRef.unsafe.make(2) } val intervals: FiberRef[Int] = Unsafe.unsafe { implicit unsafe => FiberRef.unsafe.make(3) } def run = for { _ <- retries.set(5) <&> intervals.set(3) _ <- retries.get.debug("final retries value") _ <- intervals.get.debug("final intervals value") } yield () } ``` The output of this program is: ```scala final retries value: 5 final intervals value: 3 ``` This illustrates that by incorporating more `FiberRef`s, we can concurrently update the underlying configuration values without any problems. Since the two configurations are interconnected, it might be beneficial to create a new data type utilizing `Map[String, Int]`. This approach eliminates the necessity of encoding retry configurations in two distinct `FiberRef`s: ```scala object Main extends ZIOAppDefault { val retryConfig: FiberRef[Map[String, Int]] = Unsafe.unsafe { implicit unsafe => FiberRef.unsafe.make( Map( "retries" -> 3, "intervals" -> 2 ) ) } def withRetry(n: Int) = retryConfig.update(_.updated("retries", n)) def withIntervals(n: Int) = retryConfig.update(_.updated("intervals", n)) def run = for { - <- withRetry(5) <&> withIntervals(3) _ <- retryConfig.get.debug("retryConfig") } yield () } ``` Unfortunately, with this change, the output does not align with our intended outcome: ```scala retryConfig: Map(retries -> 3, intervals -> 3) ``` The intervals have been successfully updated, but the retries remain unchanged. Why is this the case? It's because both fibers are overwriting the same state, resulting in the corruption of the final value. In the previous scenario, the updates are as follows: ```scala Parent fiber: Map(retries -> 3, intervals -> 2) Left fiber: Map(retries -> 5, intervals -> 2) right fiber: Map(retries -> 3, intervals -> 3) Parent fiber joins the left fiber: Map(retries -> 5, intervals -> 2) Parent fiber joins the right fiber: Map(retries -> 3, intervals -> 3) ``` So, this is the reason why the retries value is not updated and ends up with the wrong value. The retries value is clobbered by the right fiber when it joins the parent fiber. To solve this problem, we need a way to compose the updates. We need to be able to say, 'update the retries to 5 and then update the intervals to 3' or, conversely, 'update the intervals to 3 and then update the retries to 5'. We need to be able to compose updates. This is where compositional updates and the patch theory come into play. ### Differ and Patch Before we dive into the code, let's try to understand some terminology: ```scala trait Differ[Value, Patch] { def combine(first: Patch, second: Patch): Patch def diff(oldValue: Value, newValue: Value): Patch def empty: Patch def patch(patch: Patch)(oldValue: Value): Value } ``` By having an instance of `Differ[Value, Patch]`, we have the ability to do the following: 1. We can **diff** two values of type `Value` to generate a `Patch`. What does `Patch` mean? `Patch` is a data type that signifies the modifications made from one value to another. We can envision a patch as a "diff" between two values. 2. With **combine** function we can provide two `Patch`s and combines them into a single `Patch`. This is useful for composing updates. For example, if we have a `Patch` that updates the `retries` to 5 and another `Patch` that updates the `intervals` to 3, we can combine them into a single `Patch` that updates both the `retries` and the `intervals`. 3. Using the **patch** function we can apply a `Patch` to a value to produce a new value. 4. The **empty** function gives us a `Patch` that represents no changes. To implement a `Differ` for a data type, we need to implement these 4 functions. We have four laws associated with any `Differ` value of type `Differ[Value, Patch]`: 1. The `combine` function is associative, which means that combining two patches and then combining the result with a third patch is the same as combining the first patch with the combination of the second and third patches. 2. Combining a patch with an empty patch is the same as the patch itself. 3. Diffing a value with itself produces an empty patch. 4. Diffing two values and then patching the first value with the resulting patch results in the second value. 5. Patching a value with an empty patch results in the original value. ZIO includes some utilities which helps us to create `Differ` instances for more complex data types: - Instances of `Differ` for common data types, such as `Map`, `Set`, and `Chunk`. - `Differ.update[A]`, which constructs a differ that diffs two values by returning a function that sets the value to the new value. - `Differ.map`, which constructs a map differ from a differ which knows how to diff the values of the map. - `Differ#zip`, is used to combine two differs into a single differ that works on a tuple of values. - `Differ#orElseEither`, is used to combine two differs into a single differ that works on an `Either` of two values. - Using `Differ#transform`, we can convert a differ of one type (Value1) to a differ of another type (Value2) by providing two functions: one to convert the Value1 to Value2 and another to convert the Value2 to Value1. Let's implement a `Differ` for `retryConfig` which is a FiberRef of type `Map[String, Int]`: ```scala val differ = Differ.map[String, Int, Int => Int](Differ.update[Int]) val patch1 = differ.diff(Map("retries" -> 3), Map("retries" -> 5)) val patch2 = differ.diff(Map("intervals" -> 2), Map("intervals" -> 3)) val combined = differ.combine(patch1, patch2) val result = differ.patch(combined)(Map("retries" -> 3, "intervals" -> 2)) println(result) ``` The output is as follows: ```scala Map(retries -> 5, intervals -> 3) ``` ### First Solution: Compositional Updates For `Map[String, Int]` Data Type In previous section, we have successfully updated the `retries` and `intervals` values using compositional updates. Now we can use this differ to make the updates of our `FiberRef` composable: ```scala object Main extends ZIOAppDefault { val differ = Differ.map[String, Int, Int => Int](Differ.update[Int]) val retryConfig: FiberRef[Map[String, Int]] = Unsafe.unsafe { implicit unsafe => FiberRef.unsafe.makePatch[Map[String, Int], Differ.MapPatch[ String, Int, Int => Int ]]( Map( "retries" -> 3, "intervals" -> 2 ), differ = differ, fork0 = differ.empty ) } def withRetry(n: Int): UIO[Unit] = retryConfig.update(_.updated("retries", n)) def withIntervals(n: Int): UIO[Unit] = retryConfig.update(_.updated("intervals", n)) def run = { for { _ <- withRetry(5) <&> withIntervals(3) _ <- retryConfig.get.debug("retryConfig") } yield () } } ``` The output is as follows: ```scala retryConfig: Map(retries -> 5, intervals -> 3) ``` :::note Please note that as the `combine` operation of `Differ` is associative, the order of the updates does not change the result. This is a very important property of compositional updates in concurrent environments where multiple fibers updating the same value when they join, but the order of the joins is not deterministic. ::: ### Second Solution: Compositional Updates For The `RetryConfig` Case Class We can take this example one step further and create a type-safe configuration data type for `RetryConfig` using scala case classes: ```scala case class RetryConfig( retries: Int, intervals: Int ) ``` We can create a `Differ` for `RetryConfig` using the `Differ#transform` function: ```scala val differ: Differ[RetryConfig, (Int => Int, Int => Int)] = Differ .update[Int] .zip(Differ.update[Int]) .transform( { case (x, y) => RetryConfig.apply(x, y) }, retryConfig => (retryConfig.retries, retryConfig.intervals) ) ``` Now, as same as before, we can use this `differ` to make the updates of our new `FiberRef` composable: ```scala object Main extends ZIOAppDefault { val retryConfig: FiberRef[RetryConfig] = Unsafe.unsafe { implicit unsafe => FiberRef.unsafe.makePatch[RetryConfig, (Int => Int, Int => Int)]( initialValue0 = RetryConfig( retries = 3, intervals = 2 ), differ = differ, fork0 = differ.empty ) } def withRetry(n: Int) = retryConfig.update(_.copy(retries = n)) def withIntervals(n: Int) = retryConfig.update(_.copy(intervals = n)) def run = for { _ <- withRetry(5) <&> withIntervals(3) _ <- retryConfig.get.debug("retryConfig") } yield () } ``` --- ## Global Shared State Using Ref One of the common use cases for `Ref` is to manage the state of applications, especially in concurrent environments. We can use the `Ref` data type, which is a purely functional description of a mutable reference. > **Note:** > > In this section, we will only cover the basic usage of the `Ref` data type. To learn more details about the `Ref`, especially its usage in concurrent programming, please refer to the [`Ref`](../concurrency/ref.md) page on the concurrency section. In the previous page, we have learned how to use recursive functions to manage the state of our application. However, this approach has the following drawbacks: - We cannot share the state between multiple fibers. - Sometime, writing the application logic is a bit tedious. It is somehow awkward to pass the state using function parameters. Thanks to the `Ref` data type, we can easily use the `Ref` data type to manage the state of our application, whether we need concurrency or not. In the previous section, we learned that we can have state management, even for effectful operations. Here is the last example we tried: ```scala def inputNames: ZIO[Any, String, List[String]] = { def loop(names: List[String]): ZIO[Any, String, List[String]] = { Console.readLine("Please enter a name or `q` to exit: ").orDie.flatMap { case "q" => ZIO.succeed(names) case name => loop(names appended name) } } loop(List.empty[String]) } ``` This code can be rewritten using the `Ref` type, which is simpler than the previous one: ```scala def getNames: ZIO[Any, String, List[String]] = Ref.make(List.empty[String]) .flatMap { ref => Console .readLine("Please enter a name or 'q' to exit: ") .orDie .repeatWhileZIO { case "q" => ZIO.succeed(false) case name => ref.update(_ appended name).as(true) } *> ref.get } ``` First, we created a mutable reference to the initial state value, which is an empty list. Then, we read from the console repeatedly until the user enters the "q" command. Finally, we got the value of the reference and returned it. > **Note:** > > All the operations on the `Ref` data type are effectful. So when we are reading from or writing to a `Ref`, we are performing an effectful operation. Now that we have learned how to use the `Ref` data type, we can use it to manage the state concurrently. For example, assume while we are reading from the console, we have another fiber that is trying to update the state from a different source: ```scala def getNames: ZIO[Any, String, List[String]] = for { ref <- Ref.make(List.empty[String]) f1 <- Console .readLine("Please enter a name or 'q' to exit: ") .orDie .repeatWhileZIO { case "q" => ZIO.succeed(false) case name => ref.update(_ appended name).as(true) }.fork f2 <- ZIO.foreachDiscard(Seq("John", "Jane", "Joe", "Tom")) { name => ref.update(_ appended name) *> ZIO.sleep(1.second) } .fork _ <- f1.join _ <- f2.join v <- ref.get } yield v ``` ## Counter Example Let's write a counter using the `Ref` data type: ```scala case class Counter(value: Ref[Int]) { def inc: UIO[Unit] = value.update(_ + 1) def dec: UIO[Unit] = value.update(_ - 1) def get: UIO[Int] = value.get } object Counter { def make: UIO[Counter] = Ref.make(0).map(Counter(_)) } ``` Here is the usage example of the `Counter`: ```scala object MainApp extends ZIOAppDefault { def run = for { c <- Counter.make _ <- c.inc _ <- c.inc _ <- c.dec _ <- c.inc v <- c.get _ <- ZIO.debug(s"This counter has a value of $v.") } yield () } ``` We can use this counter in a concurrent environment, e.g. in a RESTful API to count the number of requests. But for just an example, let's concurrently update the counter: ```scala object MainApp extends ZIOAppDefault { def run = for { c <- Counter.make _ <- c.inc <&> c.inc <&> c.dec <&> c.inc v <- c.get _ <- ZIO.debug(s"This counter has a value of $v.") } yield () } ``` --- ## Introduction to State Management in ZIO When we write a program, more often we need to keep track of some sort of state during the execution of the program. If an object has a state, its behavior is influenced by passing the time. Here are some examples: - **Counter**— Assume a RESTful API, which has a set of endpoints, and wants to keep track of how many requests have been made to each endpoint. - **Bank Account Balance**— Each bank account has a balance, and it can be deposited or withdrawn. So its value is changing over time. - **Temperature**— The temperature of a room is changing over time. - **List length**— When we are iterating over a list of items, we might need to keep track of the number of items we have seen so far. So during the calculation of the length of the list, we need an intermediate state that records the number of items we have seen so far. In imperative programming, one common way to store the state is using a variable. So we can update their values in place. But this approach can introduce bugs, especially when the state is shared between multiple components. So it is better to avoid using variables to keep track of the state. From the aspect of concurrency, we have two general approaches to maintaining the state in functional programming: 1. **[Recursion](state-management-using-recursion.md)**— In this approach, we can update the state by passing the new state to the next component. This is a very easy way to maintain the state, but it can't be used in a concurrent environment, because we can't share the state between multiple fibers. 2. Concurrent— The concurrent state management is also has two variant, global and fiber-local state management: 1. **[Global Shared State](global-shared-state.md)**- ZIO has a powerful data type called `Ref`, which is the description of a mutable reference. We can use `Ref` to share the state between multiple fibers, e.g. producer and consumer components. 2. **[Fiber-local State](fiber-local-state.md)**— ZIO provides two data types called `FiberRef` and `ZState` that can be used to maintain the state in a concurrent environment, but each fiber has its own state. Their states are not shared between other fibers. This prevents them from clobbering each other's state. In this section, we will talk about these approaches. --- ## State Management Using Recursion This is a very common pattern to use variables to keep track of the state. For example, to calculate the length of a list, we can store intermediate results inside the `count` variable: ```scala def length[T](list: List[T]): Int = { var count = 0 for (_ <- list) count += 1 count } ``` But in functional programming, we avoid using variables to keep track of the state. Instead, we use other techniques. One common technique is to pass the new state as an argument to the next function. After we created a new version of the state, we pass it to the next function. We do this until we have reached the final state. Assume we have the following code: ```scala var state = 5 state = state + 1 state = state * 2 state = state * state println(state) // Output: 144 ``` We can rewrite it as a series of transformations, in which new states are passed to the next function: ```scala def foo(state: Int): Int = bar(state + 1) def bar(state: Int): Int = baz(state * 2) def baz(state: Int): Int = state * state println(foo(5)) // 144 // Output: 144 ``` Now, what if we wanted to apply the transformation multiple times to a given state? We can combine this technique with recursive functions and call the function multiple times. For example, we use function arguments to pass the state to the next function, using recursive calls. Assume we have a `length` function that returns the length of a list as below: ```scala def length[T](list: List[T]): Int = { var count = 0 for (_ <- list) count += 1 count } ``` If we want to convert the above function to a series of state transformations, first, we need to diagnose what is the state? One obvious answer is the `count` variable. The `count` variable is the state of the above function that keeps track of the length of the list. Another non-obvious state is the remainder of the list that is not yet processed during the list traversal. So we can model the state composed of the `count` and the `remainder` of the list: ```scala case class State[T](count: Int, remainder: List[T]) ``` Now we are ready to write the state transformation function called `loop` as below: ```scala case class State[T](count: Int, remainder: List[T]) def loop[T](state: State[T]): State[T] = { state.remainder match { case Nil => state case _ :: tail => loop(State(state.count + 1, tail)) } } ``` Here are series of `loop` calls, when we call it with the `State(0, List("a", "b", "c", "d"))` state: ```scala loop(State(0, List("a", "b", "c", "d"))) loop(State(1, List("b", "c", "d"))) loop(State(2, List("c", "d"))) loop(State(3, List("d"))) loop(State(4, List())) // Output: // State(4, List()) ``` Let's do some small modification to the `loop` function and use it inside the `length` function: ```scala def length[T](list: List[T]): Int = { def loop(list: List[T], count: Int): Int = { list match { case Nil => count case _ :: tail => loop(tail, count + 1) } } loop(list, 0) } ``` The same pattern can be used when we have side effects. Assume we have a function that tries to read names from the input, until the user enters the "q" command indicating the end of the input. We can write the function like this: ```scala def getNames: List[String] = { def getName() = readLine("Please enter a name or 'q' to exit: ") var names = List.empty[String] var input = getName() while (input != "q") { names = names appended input input = getName() } names } ``` Using the previous pattern, we can eliminate the need to use variables: ```scala def getNames: Seq[String] = { def loop(names: List[String]): List[String] = { val name = readLine("Please enter a name or 'q' to exit: ") if (name == "q") names else loop(names appended name) } loop(List.empty[String]) } ``` But, there is also a problem with the previous solution. The `getName` is not referentially transparent. In order to make it free of side effects, we can use `ZIO` to describe any effectual operation: ```scala def inputNames: ZIO[Any, String, List[String]] = { def loop(names: List[String]): ZIO[Any, String, List[String]] = { Console.readLine("Please enter a name or `q` to exit: ").orDie.flatMap { case "q" => ZIO.succeed(names) case name => loop(names appended name) } } loop(List.empty[String]) } ``` On this page, we have learned how to have stateful computations in our programs using recursion. However, this approach is not suitable for concurrent programs, where multiple fibers want to change the state of the program concurrently. Let's move on to the next page, where we will discuss stateful computation over concurrent programs. --- ## ZState `ZState[S]` models a value of type `S` that can be read from and written to during the execution of an effect. This is a higher-level construct built on top of [`FiberRef`](fiberref.md) and the environment type to support using ZIO where we might have traditionally used state monad transformers. Let's try a simple example of using `ZState`: ```scala object ZStateExample extends zio.ZIOAppDefault { val myApp: ZIO[ZState[Int], IOException, Unit] = for { s <- ZIO.service[ZState[Int]] _ <- s.update(_ + 1) _ <- s.update(_ + 2) state <- s.get _ <- Console.printLine(s"current state: $state") } yield () def run = ZIO.stateful(0)(myApp) } ``` The idiomatic way to work with `ZState` is as part of the environment using operators defined on `ZIO` to access the `ZState` from the environment, and finally, allocate the initial state using the `ZIO.stateful` operator. Because we typically use `ZState` as part of the environment, it is recommended to define our own state type `S` such as `MyState` rather than using a type such as `Int` to avoid the risk of ambiguity: ```scala final case class MyState(counter: Int) object ZStateExample extends zio.ZIOAppDefault { val myApp: ZIO[ZState[MyState], IOException, Unit] = for { counter <- ZIO.service[ZState[MyState]] _ <- counter.update(state => state.copy(counter = state.counter + 1)) _ <- counter.update(state => state.copy(counter = state.counter + 2)) state <- counter.get _ <- Console.printLine(s"Current state: $state") } yield () def run = ZIO.stateful(MyState(0))(myApp) } ``` The `ZIO` data type also has some helper methods to work with `ZState` as the environment of `ZIO` effect such as `ZIO.updateState`, `ZIO.getState`, and `ZIO.getStateWith`: ```scala final case class MyState(counter: Int) val myApp: ZIO[ZState[MyState], IOException, Int] = for { _ <- ZIO.updateState[MyState](state => state.copy(counter = state.counter + 1)) _ <- ZIO.updateState[MyState](state => state.copy(counter = state.counter + 2)) state <- ZIO.getStateWith[MyState](_.counter) _ <- Console.printLine(s"Current state: $state") } yield state ``` An important note about `ZState` is that it is on top of the `FiberRef` data type. So it will inherit its behavior from the `FiberRef`. For example, when a fiber is going to join to its parent fiber, its state will be merged with its parent state: ```scala case class MyState(counter: Int) object ZStateExample extends ZIOAppDefault { val myApp = for { _ <- ZIO.updateState[MyState](state => state.copy(counter = state.counter + 1)) fiber <- (for { _ <- ZIO.updateState[MyState](state => state.copy(counter = state.counter + 1)) state <- ZIO.getState[MyState] _ <- Console.printLine(s"Current state inside the forked fiber: $state") } yield ()).fork _ <- ZIO.updateState[MyState](state => state.copy(counter = state.counter + 5)) state1 <- ZIO.getState[MyState] _ <- Console.printLine(s"Current state before merging the fiber: $state1") _ <- fiber.join state2 <- ZIO.getState[MyState] _ <- Console.printLine(s"The final state: $state2") } yield () def run = ZIO.stateful(MyState(0))(myApp) } ``` The output of running this snippet code would be as below: ``` Current state before merging the fiber: MyState(6) Current state inside the forked fiber: MyState(2) The final state: MyState(2) ``` --- ## Introduction to Software Transactional Memory ## Overview ZIO supports Software Transactional Memory (STM) which is a modular composable concurrency data structure. It allows us to combine and compose a group of memory operations and perform all of them in one single atomic operation. Software Transactional Memory is an abstraction for concurrent communications. The main benefits of STM are composability and modularity. We can write concurrent abstractions that can be composed with any other abstraction built using STM, without exposing the details of how our abstraction ensures safety. This is typically not the case with the locking mechanism. The idea of the transactional operation is not new, they have been the fundamental of distributed systems, and those databases that guarantee us an ACID property. Software transactional memory is just all about memory operations. All operations are performed on memory. It is not related to a remote system or a database. Very similar to the database concept of ACID property, but the _durability_, is missing which doesn't make sense for in-memory operations. In transactional memory, we get these aspects of ACID properties: - **Atomicity** — On write operations, we want _atomic update_, which means the update operation either should run at once or not at all. - **Consistency** — On read operations, we want a _consistent view_ of the state of the program that ensures us all references to the state, get the same value whenever they get the state. - **Isolated** — If we have multiple updates, we need to perform these updates in isolated transactions. So each transaction doesn't affect other concurrent transactions. No matter how many fibers are running any number of transactions. None of them have to worry about what is happening in the other transactions. The ZIO STM API is inspired by Haskell's [STM library](http://hackage.haskell.org/package/stm-2.5.0.0/docs/Control-Concurrent-STM.html) although the implementation in ZIO is completely different. ## The Problem Let's start from a simple `inc` function, which takes a mutable reference of `Int` and increases it by `amount`: ```scala def inc(counter: Ref[Int], amount: Int) = for { c <- counter.get _ <- counter.set(c + amount) } yield c ``` If there is only one fiber in the world, it is not a problem. This function sounds correct. But what happens if in between reading the value of the counter and setting a new value, another fiber comes and mutates the value of the counter? Another fiber is just updating the counter just after we read the counter. So this function is subject to a race condition, we can test that with the following program: ```scala for { counter <- Ref.make(0) _ <- ZIO.collectAllPar(ZIO.replicate(10)(inc(counter, 1))) value <- counter.get } yield (value) ``` The above program runs 10 concurrent fibers to increase the counter value. However, we cannot expect this program to always return 10 as a result. To fix this issue, we need to perform the `get` and `set` operations atomically. The `Ref` data type some other API like `update`, `updateAndGet`, and `modify` which perform the reading and writing atomically: ```scala def inc(counter: Ref[Int], amount: Int) = counter.updateAndGet(_ + amount) ``` The most important note about the `modify` operation is that it doesn't use pessimistic locking. It doesn't use any locking primitives for the critical section. It has an optimistic assumption about occurring collisions. The `modify` function takes these three steps: 1. It assumes that other fibers don't change the shared state and don't interfere in most cases. So it read the shared state without using any locking primitives. 2. It should prepare itself for the worst-case scenarios. If another fiber is accessing the data at the same time, what would happen? Therefore, when we write a new value, it should check everything. It must ensure that it sees a consistent state of the universe, and if it does, then it can change that value. 4. If it encounters an inconsistent value, it shouldn't continue. So it aborts updating the shared state with an invalidated assumption. It should retry the `modify` operation with an updated state. Let's see how the `modify` function of `Ref` is implemented without any locking mechanism: ```scala final case class Ref[A](value: AtomicReference[A]) { self => def modify[B](f: A => (B, A)): UIO[B] = ZIO.succeed { var loop = true var b: B = null.asInstanceOf[B] while (loop) { val current = value.get val tuple = f(current) b = tuple._1 loop = !value.compareAndSet(current, tuple._2) } b } } ``` As we see, the `modify` operation is implemented in terms of the `compare-and-swap` operation which helps us to perform read and update atomically. Let's rename the `inc` function to the `deposit` as follows to try the classic problem of transferring money from one account to another: ```scala def deposit(accountBalance: Ref[Int], amount: Int) = accountBalance.update(_ + amount) ``` And the `withdraw` function: ```scala def withdraw(accountBalance: Ref[Int], amount: Int) = accountBalance.update(_ - amount) ``` It seems pretty good, but we also need to check that there is sufficient balance in the account to withdraw. So let's add an invariant to check that: ```scala def withdraw(accountBalance: Ref[Int], amount: Int) = for { balance <- accountBalance.get _ <- if (balance < amount) ZIO.fail("Insufficient funds in you account") else accountBalance.update(_ - amount) } yield () ``` What if in between checking and updating the balance, another fiber comes and withdraws all money in the account? This solution has a bug. It has the potential to reach a negative balance. Suppose we finally reached a solution to do withdraw atomically, the problem remains. We need a way to compose `withdraw` with `deposit` atomically to create a `transfer function: ```scala def transfer(from: Ref[Int], to: Ref[Int], amount: Int) = for { _ <- withdraw(from, amount) _ <- deposit(to, amount) } yield () ``` In the above example, even if we assume that the `withdraw` and `deposit` are atomic, we can't compose these two transactions. They produce bugs in a concurrent environment. This code doesn't guarantee us that both `withdraw` and `deposit` are performed in one single atomic operation. Other fibers which are executing this `transfer` method can override the shared state and introduce a race condition. We need a solution to **atomically compose transactions**. This is where software transactional memory comes into play. ## Composable Concurrency Software transactional memory provides us a way to compose multiple transactions and perform them in one single transaction. Let's continue our last effort to convert our `withdraw` method to be one atomic operation. To solve the problem using STM, we replace `Ref` with `TRef`. `TRef` stands for _Transactional Reference_; it is a mutable reference contained in the `STM` world. `STM` is a monadic data structure that represents an effect that can be performed transactionally: ```scala def withdraw(accountBalance: TRef[Int], amount: Int): STM[String, Unit] = for { balance <- accountBalance.get _ <- if (balance < amount) STM.fail("Insufficient funds in you account") else accountBalance.update(_ - amount) } yield () ``` Although the `deposit` operation is atomic, to be able to compose with `withdraw` we need to refactor it to take `TRef` and return `STM`: ```scala def deposit(accountBalance: TRef[Int], amount: Int): STM[Nothing, Unit] = accountBalance.update(_ + amount) ``` In the `STM` world we can compose all operations and at the end of the world, we perform all of them in one single operation atomically. To be able to compose `withdraw` with `deposit` we need to stay in the `STM` world. Therefore, we didn't perform `STM.atomically` or `STM#commit` methods on each of them. Now we can define the `transfer` method by composing these two functions in the `STM` world and converting them into the `IO` atomically: ```scala def transfer(from: TRef[Int], to: TRef[Int], amount: Int): IO[String, Unit] = STM.atomically { for { _ <- withdraw(from, amount) _ <- deposit(to, amount) } yield () } ``` Assume we are in the middle of transferring money from one account to the other. If we withdraw the first account but haven't deposited the second account, that kind of intermediate state is not visible to any external fibers. The transaction is completely successful if there are no conflicting changes. If there are any conflicts or conflicting changes then the whole transaction, the entire STM will be retried. ## How Does it Work? The `STM` uses the same idea as the `Ref#modify` function but with a composability feature. The main goal of `STM` is to provide a mechanism to compose multiple transactions and perform them in one single atomic operation. The mechanism behind the compositional part is obvious. The `STM` has its own world. It has lots of useful combinators like `flatMap` and `orElse` to compose multiple `STM`s and create more elegant ones. After we perform a transaction with `STM#commit` or `STM.atomically` the runtime system does the following steps. These steps are not exactly accurate, but they draw an outline of what happens during the transaction: 1. **Starting a Transaction** — When we start a transaction, the runtime system creates a virtual space to keep track of the transaction logs which is build up by recording the reads and tentative writes that the transaction will perform during the transaction steps. 2. **Virtual Execution** — The runtime starts speculating the execution of transactions on every read and write operation. It has two internal logs; the read and the write log. On the read log, it saves the version of all variables it reads during the intermediate steps, and on the write log, it saves the intermediate result of the transaction. It doesn't change the shared state on the main memory. Anything that is inside an atomic block is not executed immediately, it's executed in the virtual world, just by putting stuff in the internal log, not in the main memory. In this particular model, we guarantee that all computations are isolated from one another. 3. **Commit Phase (Real Execution)** — When it comes to the end of the transaction the runtime system should check everything it has read. It should make sure that it sees a consistent state of the universe and if it has, then it atomically commits. As the STM is optimistic, it assumes that in the middle of a transaction, the chance of interfering with the shared state by other fibers is very rare. But it must ready itself for the worst cases. It should validate its assumption in the final stage. It checks whether the transactional variables involved were modified by any other threads or not. If its assumption got invalidated in the meanwhile of the transaction, it should abandon the transaction and retry it again. It jumps to the start of the transaction with the original and default values and tries again until it succeeds; This is necessary to resolve conflicts. Otherwise, if there is no conflict, it commits the final value atomically to the memory and succeeds. From the point of view of other fibers, all values in memory exchange in one blink of an eye. It's all atomic. Everything done within a transaction to other transactions looks like it happens at once or not at all. So no matter how many pieces of memory it touches during the transaction. From the other transaction perspective, all of these changes happen at once. ## STM Data Types Like the `ZIO` data type, the `ZSTM` has some type aliases as follows: ```scala type RSTM[-R, +A] = ZSTM[R, Throwable, A] type URSTM[-R, +A] = ZSTM[R, Nothing, A] type STM[+E, +A] = ZSTM[Any, E, A] type USTM[+A] = ZSTM[Any, Nothing, A] type TaskSTM[+A] = ZSTM[Any, Throwable, A] ``` There are a variety of transactional data structures that can take part in an STM transaction: - **[TArray](tarray.md)** - A `TArray[A]` is an array of mutable references that can participate in transactions. - **[TRandom](trandom.md)** — `TRandom` is a random service that provides utilities to generate random numbers, which can participate in STM transactions. - **[TSet](tset.md)** - A `TSet` is a mutable set that can participate in transactions. - **[TMap](tmap.md)** - A `TMap[A]` is a mutable map that can participate in transactions. - **[TRef](tref.md)** - A `TRef` is a mutable reference to an immutable value that can participate in transactions. - **[TPriorityQueue](tpriorityqueue.md)** - A `TPriorityQueue[A]` is a mutable priority queue that can participate in transactions. - **[TPromise](tpromise.md)** - A `TPromise` is a mutable reference that can be set exactly once and can participate in transactions. - **[TQueue](tqueue.md)** - A `TQueue` is a mutable queue that can participate in transactions. - **[TReentrantLock](treentrantlock.md)** - A `TReentrantLock` is a reentrant read / write lock that can be composed. - **[TSemaphore](tsemaphore.md)** - A `TSemaphore` is a semaphore that can participate in transactions. - **[THub](thub.md)** - A `THub` is a hub that can participate in STM transactions. Since STM places a great emphasis on compositionality, we can build upon these data structures and define our very own concurrent data structures. For example, we can build a transactional priority queue using `TRef`, `TMap` and `TQueue`. ## Advantage of Using STM 1. **Composable Transaction** — Combining atomic operations using locking-oriented programming is almost impossible. ZIO provides the `STM` data type, which has lots of combinators to compose transactions. 2. **Declarative** — ZIO STM is completely declarative. It doesn't require us to think about low-level primitives. It doesn't force us to think about the ordering of locks. Reasoning concurrent programs in a declarative fashion is very simple. We can just focus on the logic of our program and run it in a concurrent environment deterministically. The user code is much simpler of course because it doesn't have to deal with the concurrency at all. 3. **Optimistic Concurrency** — In most cases, we are allowed to be optimistic unless there is tremendous contention. So if we haven't tremendous contention it really pays to be optimistic. It allows a higher volume of concurrent transactions. 4. **Lock-Free** — All operations are non-blocking using lock-free algorithms. 5. **Fine-Grained Locking**— Coarse-grained locking is very simple to implement, but it has a negative impact on performance, while fine-grained locking significantly has better performance, but it is very cumbersome, sophisticated, and error-prone even for experienced programmers. We would like to have the ease of use of coarse-grain locking, but at the same time, we would like to have the efficiency of fine-grain locking. ZIO provides several data types which are a very coarse way of using concurrency, but they are implemented as if every single word were lockable. So the granularity of concurrency is fine-grained. It increases the performance and concurrency. For example, if we have two fibers accessing the same `TArray`, one of them reads and writes on the first index of our array, and another one reads and writes to the second index of that array, they will not conflict. It is just like as if we were locking the indices, not the whole array. ## Implication of Using STM 1. **Running I/O Inside STM**— There is a strict boundary between the `STM` world and the `ZIO` world. This boundary propagates even deeper because we are not allowed to execute arbitrary effects in the `STM` universe. Performing side effects and I/O operations inside a transaction is problematic. In the `STM` the only effect that exists is the `STM` itself. We cannot print something or launch a missile inside a transaction as it will nondeterministically get printed on every reties that transaction does that. 2. **Large Allocations** — We should be very careful in choosing the best data structure for using STM operations. For example, if we use a single data structure with `TRef` and that data structure occupies a big chunk of memory. Every time we are updating this data structure during the transaction, the runtime system needs a fresh copy of this chunk of memory. 3. **Running Expensive Operations**— The beautiful feature of the `retry` combinator is when we decide to retry the transaction, the `retry` avoids the busy loop. It waits until any of the underlying transactional variables have changed. However, we should be careful about running expensive operations multiple times. --- ## STM An `STM[E, A]` represents an effect that can be performed transactionally resulting in a failure `E` or a success `A`. There is a more powerful variant `ZSTM[R, E, A]` which supports an environment type `R` like `ZIO[R, E, A]`. The `STM` (and `ZSTM` variant) data-type is _not_ as powerful as the `ZIO[R, E, A]` datatype as it does not allow you to perform arbitrary effects. These are because actions inside STM actions can be executed an arbitrary amount of times (and rolled-back as well). Only STM actions and pure computation may be performed inside a memory transaction. No STM actions can be performed outside a transaction, so you cannot accidentally read or write a transactional data structure outside the protection of `STM.atomically` (or without explicitly `commit`ting the transaction). For example: ```scala def transferMoney(from: TRef[Long], to: TRef[Long], amount: Long): STM[String, Long] = for { senderBal <- from.get _ <- if (senderBal < amount) STM.fail("Not enough money") else STM.unit _ <- from.update(existing => existing - amount) _ <- to.update(existing => existing + amount) recvBal <- to.get } yield recvBal val program: IO[String, Long] = for { sndAcc <- STM.atomically(TRef.make(1000L)) rcvAcc <- STM.atomically(TRef.make(0L)) recvAmt <- STM.atomically(transferMoney(sndAcc, rcvAcc, 500L)) } yield recvAmt ``` `transferMoney` describes an atomic transfer process between a sender and a receiver. The transaction will fail if the sender does not have enough of money in their account. This means that individual accounts will be debited and credited atomically. If the transaction fails in the middle, the entire process will be rolled back, and it will appear that nothing has happened. Here, we see that `STM` effects compose using a for-comprehension and that wrapping an `STM` effect with `STM.atomically` (or calling `commit` on any STM effect) turns the `STM` effect into a `ZIO` effect which can be executed. STM transactions compose sequentially. By using `STM.atomically` (or `commit`), the programmer identifies atomic transaction in the sense that the entire set of operations within `STM.atomically` appears to take place indivisibly. ## Errors `STM` supports errors just like `ZIO` via the error channel. In `transferMoney`, we saw an example of an error (`STM.fail`). Errors in `STM` have abort semantics: if an atomic transaction encounters an error, the transaction is rolled back with no effect. ## `retry` `STM.retry` is central to making transactions composable when they may block. For example, if we wanted to ensure that the money transfer took place when the sender had enough of money (instead of failing right away), we can use `STM.retry` instead: ```scala def transferMoneyNoMatterWhat(from: TRef[Long], to: TRef[Long], amount: Long): STM[String, Long] = for { senderBal <- from.get _ <- if (senderBal < amount) STM.retry else STM.unit _ <- from.update(existing => existing - amount) _ <- to.update(existing => existing + amount) recvBal <- to.get } yield recvBal ``` `STM.retry` will abort and retry the entire transaction until it succeeds (instead of failing like the previous example). Note that the transaction will only be retried when one of the underlying transactional data structures have been changed. There are many other variants of the `STM.retry` combinator like `STM.check` so rather than writing `if (senderBal < amount) STM.retry else STM.unit`, you can replace it with `STM.check(senderBal < amount)`. ## Composing alternatives STM transactions compose sequentially so that both STM effects are executed. However, STM transactions can also compose transactions as alternatives so that only one STM effect is executed by making use of `orTry` on STM effects. Provided we have two STM effects `sA` and `sB`, you can express that you would like to compose the two using `sA orTry sB`. The transaction would first attempt to run `sA` and if it retries then `sA` is abandoned with no effect and then `sB` runs. Now if `sB` also retries then the entire call retries. However, it waits for the transactional data structures to change that are involved in either `sA` or `sB`. Using `orTry` is an elegant technique that can be used to determine whether or not an STM transaction needs to block. For example, we can take `transferMoneyNoMatterWhat` and turn it into an STM transaction that will fail immediately if the sender does not have enough of money instead of retrying by doing: ```scala def transferMoneyFailFast(from: TRef[Long], to: TRef[Long], amount: Long): STM[String, Long] = transferMoneyNoMatterWhat(from, to, amount) orTry STM.fail("Sender does not have enough of money") ``` This will cause the transfer to fail immediately if the sender does not have money because of the semantics of `orTry`. --- ## TArray `TArray` is an array of mutable references that can participate in transactions in STM. ## Create a TArray Creating an empty `TArray`: ```scala val emptyTArray: STM[Nothing, TArray[Int]] = TArray.empty[Int] ``` Or creating a `TArray` with specified values: ```scala val specifiedValuesTArray: STM[Nothing, TArray[Int]] = TArray.make(1, 2, 3) ``` Alternatively, you can create a `TArray` by providing a collection of values: ```scala val iterableTArray: STM[Nothing, TArray[Int]] = TArray.fromIterable(List(1, 2, 3)) ``` ## Retrieve the value from a TArray The n-th element of the array can be obtained as follows: ```scala val tArrayGetElem: UIO[Int] = (for { tArray <- TArray.make(1, 2, 3, 4) elem <- tArray(2) } yield elem).commit ``` Accessing the non-existing indexes aborts the transaction with `ArrayIndexOutOfBoundsException`. ## Update the value of a TArray Updating the n-th element of an array can be done as follows: ```scala val tArrayUpdateElem: UIO[TArray[Int]] = (for { tArray <- TArray.make(1, 2, 3, 4) _ <- tArray.update(2, el => el + 10) } yield tArray).commit ``` Updating the n-th element of an array can be done effectfully via `updateSTM`: ```scala val tArrayUpdateMElem: UIO[TArray[Int]] = (for { tArray <- TArray.make(1, 2, 3, 4) _ <- tArray.updateSTM(2, el => STM.succeed(el + 10)) } yield tArray).commit ``` Updating the non-existing indexes aborts the transaction with `ArrayIndexOutOfBoundsException`. ## Transform elements of a TArray The transform function `A => A` allows computing a new value for every element in the array: ```scala val transformTArray: UIO[TArray[Int]] = (for { tArray <- TArray.make(1, 2, 3, 4) _ <- tArray.transform(a => a * a) } yield tArray).commit ``` The elements can be mapped effectfully via `transformSTM`: ```scala val transformSTMTArray: UIO[TArray[Int]] = (for { tArray <- TArray.make(1, 2, 3, 4) _ <- tArray.transformSTM(a => STM.succeed(a * a)) } yield tArray).commit ``` Folds the elements of a `TArray` using the specified associative binary operator: ```scala val foldTArray: UIO[Int] = (for { tArray <- TArray.make(1, 2, 3, 4) sum <- tArray.fold(0)(_ + _) } yield sum).commit ``` The elements can be folded effectfully via `foldSTM`: ```scala val foldSTMTArray: UIO[Int] = (for { tArray <- TArray.make(1, 2, 3, 4) sum <- tArray.foldSTM(0)((acc, el) => STM.succeed(acc + el)) } yield sum).commit ``` ## Perform effects for TArray elements `foreach` is used for performing an STM effect for each element in the array: ```scala val foreachTArray = (for { tArray <- TArray.make(1, 2, 3, 4) tQueue <- TQueue.unbounded[Int] _ <- tArray.foreach(a => tQueue.offer(a).unit) } yield tArray).commit ``` --- ## THub A `THub` is a transactional message hub. Publishers can publish messages to the hub and subscribers can subscribe to take messages from the hub. A `THub` is an asynchronous message hub like `Hub` but it can participate in STM transactions. APIs are almost identical, but they are in the `STM` world rather than the `ZIO` world. The fundamental operators on a `THub` are `publish` and `subscribe`: ```scala trait THub[A] { def publish(a: A): USTM[Boolean] def subscribe: USTM[TDequeue[B]] } ``` --- ## TMap A `TMap[A]` is a mutable map that can participate in transactions in STM. ## Create a TMap Creating an empty `TMap`: ```scala val emptyTMap: STM[Nothing, TMap[String, Int]] = TMap.empty[String, Int] ``` Or creating a `TMap` with specified values: ```scala val specifiedValuesTMap: STM[Nothing, TMap[String, Int]] = TMap.make(("a", 1), ("b", 2), ("c", 3)) ``` Alternatively, you can create a `TMap` by providing a collection of tuple values: ```scala val iterableTMap: STM[Nothing, TMap[String, Int]] = TMap.fromIterable(List(("a", 1), ("b", 2), ("c", 3))) ``` ## Put a key-value pair to a TMap New key-value pair can be added to the map in the following way: ```scala val putElem: UIO[TMap[String, Int]] = (for { tMap <- TMap.make(("a", 1), ("b", 2)) _ <- tMap.put("c", 3) } yield tMap).commit ``` Another way of adding an entry in the map is by using `merge`: ```scala val mergeElem: UIO[TMap[String, Int]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) _ <- tMap.merge("c", 4)((x, y) => x * y) } yield tMap).commit ``` If the key is not present in the map it behaves as a simple `put` method. It merges the existing value with the new one using the provided function otherwise. ## Remove an element from a TMap The simplest way to remove a key-value pair from `TMap` is using `delete` method that takes key: ```scala val deleteElem: UIO[TMap[String, Int]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) _ <- tMap.delete("b") } yield tMap).commit ``` Also, it is possible to remove every key-value pairs that satisfy provided predicate: ```scala val removedEvenValues: UIO[TMap[String, Int]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3), ("d", 4)) _ <- tMap.removeIf((_, v) => v % 2 == 0) } yield tMap).commit ``` Or you can keep all key-value pairs that match predicate function: ```scala val retainedEvenValues: UIO[TMap[String, Int]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3), ("d", 4)) _ <- tMap.retainIf((_, v) => v % 2 == 0) } yield tMap).commit ``` Note that `retainIf` and `removeIf` serve the same purpose as `filter` and `filterNot`. The reason for naming them differently was to emphasize a distinction in their nature. Namely, both `retainIf` and `removeIf` are destructive - calling them can modify the collection. ## Retrieve the value from a TMap Value associated with the key can be obtained as follows: ```scala val elemGet: UIO[Option[Int]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) elem <- tMap.get("c") } yield elem).commit ``` Alternatively, you can provide a default value if entry by key is not present in the map: ```scala val elemGetOrElse: UIO[Int] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) elem <- tMap.getOrElse("d", 4) } yield elem).commit ``` ## Transform entries of a TMap The transform function `(K, V) => (K, V)` allows computing a new value for every entry in the map: ```scala val transformTMap: UIO[TMap[String, Int]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) _ <- tMap.transform((k, v) => k -> v * v) } yield tMap).commit ``` Note that it is possible to shrink a `TMap`: ```scala val shrinkTMap: UIO[TMap[String, Int]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) _ <- tMap.transform((_, v) => "d" -> v) } yield tMap).commit ``` The entries can be mapped effectfully via `transformSTM`: ```scala val transformSTMTMap: UIO[TMap[String, Int]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) _ <- tMap.transformSTM((k, v) => STM.succeed(k -> v * v)) } yield tMap).commit ``` The `transformValues` function `V => V` allows computing a new value for every value in the map: ```scala val transformValuesTMap: UIO[TMap[String, Int]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) _ <- tMap.transformValues(v => v * v) } yield tMap).commit ``` The values can be mapped effectfully via `transformValuesSTM`: ```scala val transformValuesMTMap: UIO[TMap[String, Int]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) _ <- tMap.transformValuesSTM(v => STM.succeed(v * v)) } yield tMap).commit ``` Note that both `transform` and `transformValues` serve the same purpose as `map` and `mapValues`. The reason for naming them differently was to emphasize a distinction in their nature. Namely, both `transform` and `transformValues` are destructive - calling them can modify the collection. Folds the elements of a `TMap` using the specified associative binary operator: ```scala val foldTMap: UIO[Int] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) sum <- tMap.fold(0) { case (acc, (_, v)) => acc + v } } yield sum).commit ``` The elements can be folded effectfully via `foldSTM`: ```scala val foldSTMTMap: UIO[Int] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) sum <- tMap.foldSTM(0) { case (acc, (_, v)) => STM.succeed(acc + v) } } yield sum).commit ``` ## Perform effects for TMap key-value pairs `foreach` is used for performing an STM effect for each key-value pair in the map: ```scala val foreachTMap = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) tQueue <- TQueue.unbounded[String] _ <- tMap.foreach((k, v) => tQueue.offer(s"$k -> $v").unit) } yield tMap).commit ``` ## Check TMap membership Checking whether key-value pair is present in a `TMap`: ```scala val tMapContainsValue: UIO[Boolean] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) res <- tMap.contains("a") } yield res).commit ``` ## Convert TMap to a List List of tuples can be obtained as follows: ```scala val tMapTuplesList: UIO[List[(String, Int)]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) list <- tMap.toList } yield list).commit ``` List of keys can be obtained as follows: ```scala val tMapKeysList: UIO[List[String]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) list <- tMap.keys } yield list).commit ``` List of values can be obtained as follows: ```scala val tMapValuesList: UIO[List[Int]] = (for { tMap <- TMap.make(("a", 1), ("b", 2), ("c", 3)) list <- tMap.values } yield list).commit ``` --- ## TPriorityQueue A `TPriorityQueue[A]` is a mutable queue that can participate in STM transactions. A `TPriorityQueue` contains values of type `A` for which an `Ordering` is defined. Unlike a `TQueue`, `take` returns the highest priority value (the value that is first in the specified ordering) as opposed to the first value offered to the queue. The ordering of elements sharing the same priority when taken from the queue is not guaranteed. ## Creating a TPriorityQueue You can create an empty `TPriorityQueue` using the `empty` constructor: ```scala val minQueue: STM[Nothing, TPriorityQueue[Int]] = TPriorityQueue.empty ``` Notice that a `TPriorityQueue` is created with an implicit `Ordering`. By default, `take` will return the value that is first in the specified ordering. For example, in a queue of events ordered by time the earliest event would be taken first. If you want a different behavior you can use a custom `Ordering`. ```scala val maxQueue: STM[Nothing, TPriorityQueue[Int]] = TPriorityQueue.empty(Ordering[Int].reverse) ``` You can also create a `TPriorityQueue` initialized with specified elements using the `fromIterable` or `make` constructors". The `fromIterable` constructor takes a `Iterable` while the `make` constructor takes a variable arguments sequence of elements. ## Offering elements to a TPriorityQueue You can offer elements to a `TPriorityQueue` using the `offer` or `offerAll` methods. The `offerAll` method is more efficient if you want to offer more than one element to the queue at the same time. ```scala val queue: STM[Nothing, TPriorityQueue[Int]] = for { queue <- TPriorityQueue.empty[Int] _ <- queue.offerAll(List(2, 4, 6, 3, 5, 6)) } yield queue ``` ## Taking elements from a TPriorityQueue Take an element from a `TPriorityQueue` using the `take`. `take` will semantically block until there is at least one value in the queue to take. You can also use `takeAll` to immediately take all values that are currently in the queue, or `takeUpTo` to immediately take up to the specified number of elements from the queue. ```scala val sorted: STM[Nothing, Chunk[Int]] = for { queue <- TPriorityQueue.empty[Int] _ <- queue.offerAll(List(2, 4, 6, 3, 5, 6)) sorted <- queue.takeAll } yield sorted ``` You can also use `takeOption` method to take the first value from the queue if it exists without suspending or the `peek` method to observe the first element of the queue if it exists without removing it from the queue. Sometimes you want to take a snapshot of the current state of the queue without modifying it. For this the `toChunk` combinator or its variants `toList` or `toVector` are extremely helpful. These will return an immutable collection that consists of all of the elements currently in the queue, leaving the state of the queue unchanged. ## Size of a TPriorityQueue You can check the size of the `TPriorityQueue` using the `size` method: ```scala val size: STM[Nothing, Int] = for { queue <- TPriorityQueue.empty[Int] _ <- queue.offerAll(List(2, 4, 6, 3, 5, 6)) size <- queue.size } yield size ``` --- ## TPromise `TPromise` is a mutable reference that can be set exactly once and can participate in transactions in STM. ## Create a TPromise Creating a `TPromise`: ```scala val tPromise: STM[Nothing, TPromise[String, Int]] = TPromise.make[String, Int] ``` ## Complete a TPromise In order to successfully complete a `TPromise`: ```scala val tPromiseSucceed: UIO[TPromise[String, Int]] = for { tPromise <- TPromise.make[String, Int].commit _ <- tPromise.succeed(0).commit } yield tPromise ``` In order to fail a `TPromise` use: ```scala val tPromiseFail: UIO[TPromise[String, Int]] = for { tPromise <- TPromise.make[String, Int].commit _ <- tPromise.fail("failed").commit } yield tPromise ``` Alternatively, you can use `done` combinator and complete the promise by passing it `Either[E, A]`: ```scala val tPromiseDoneSucceed: UIO[TPromise[String, Int]] = for { tPromise <- TPromise.make[String, Int].commit _ <- tPromise.done(Right(0)).commit } yield tPromise val tPromiseDoneFail: UIO[TPromise[String, Int]] = for { tPromise <- TPromise.make[String, Int].commit _ <- tPromise.done(Left("failed")).commit } yield tPromise ``` Once the value is set, any following attempts to set it will result in `false`. ## Retrieve the value of a TPromise Returns the result if the promise has already been completed or a `None` otherwise: ```scala val tPromiseOptionValue: UIO[Option[Either[String, Int]]] = for { tPromise <- TPromise.make[String, Int].commit _ <- tPromise.succeed(0).commit res <- tPromise.poll.commit } yield res ``` Alternatively, you can wait for the promise to be completed and return the value: ```scala val tPromiseValue: IO[String, Int] = for { tPromise <- TPromise.make[String, Int].commit _ <- tPromise.succeed(0).commit res <- tPromise.await.commit } yield res ``` --- ## TQueue A `TQueue[A]` is a mutable queue that can participate in transactions in STM. ## Create a TQueue Creating an empty bounded `TQueue` with specified capacity: ```scala val tQueueBounded: STM[Nothing, TQueue[Int]] = TQueue.bounded[Int](5) ``` Creating an empty unbounded `TQueue`: ```scala val tQueueUnbounded: STM[Nothing, TQueue[Int]] = TQueue.unbounded[Int] ``` ## Put element(s) in a TQueue In order to put an element to a `TQueue`: ```scala val tQueueOffer: UIO[TQueue[Int]] = (for { tQueue <- TQueue.bounded[Int](3) _ <- tQueue.offer(1) } yield tQueue).commit ``` The specified element will be successfully added to a queue if the queue is not full. It will wait for an empty slot in the queue otherwise. Alternatively, you can provide a list of elements: ```scala val tQueueOfferAll: UIO[TQueue[Int]] = (for { tQueue <- TQueue.bounded[Int](3) _ <- tQueue.offerAll(List(1, 2)) } yield tQueue).commit ``` ## Retrieve element(s) from a TQueue The first element of the queue can be obtained as follows: ```scala val tQueueTake: UIO[Int] = (for { tQueue <- TQueue.bounded[Int](3) _ <- tQueue.offerAll(List(1, 2)) res <- tQueue.take } yield res).commit ``` In case the queue is empty it will block execution waiting for the element you're asking for. This behavior can be avoided by using `poll` method that will return an element if exists or `None` otherwise: ```scala val tQueuePoll: UIO[Option[Int]] = (for { tQueue <- TQueue.bounded[Int](3) res <- tQueue.poll } yield res).commit ``` Retrieving first `n` elements of the queue: ```scala val tQueueTakeUpTo: UIO[Chunk[Int]] = (for { tQueue <- TQueue.bounded[Int](4) _ <- tQueue.offerAll(List(1, 2)) res <- tQueue.takeUpTo(3) } yield res).commit ``` All elements of the queue can be obtained as follows: ```scala val tQueueTakeAll: UIO[Chunk[Int]] = (for { tQueue <- TQueue.bounded[Int](4) _ <- tQueue.offerAll(List(1, 2)) res <- tQueue.takeAll } yield res).commit ``` ## Size of a TQueue The number of elements in the queue can be obtained as follows: ```scala val tQueueSize: UIO[Int] = (for { tQueue <- TQueue.bounded[Int](3) _ <- tQueue.offerAll(List(1, 2)) size <- tQueue.size } yield size).commit ``` --- ## TRandom `TRandom` is a random service like [Random](../services/random.md) that provides utilities to generate random numbers, but they can participate in STM transactions. The `TRandom` service is the same as the `Random` service. There are no differences in operations, but all return types are in the `STM` world rather than the `ZIO` world: | Function | Input Type | Output Type | | --------------| ------------- | ----------------------------- | | `nextBoolean` | | `URSTM[TRandom, Boolean]` | | `nextBytes` | `length: Int` | `URSTM[TRandom, Chunk[Byte]]` | | `nextDouble` | | `URSTM[TRandom, Double]` | | `nextInt` | | `URSTM[TRandom, Int]` | | ... | ... | ... | When we use operations of the `TRandom` service, they add `TRandom` dependency on our `STM` data type. After committing all the transactions, we can `inject`/`provide` a `TRandom` implementation into our effect: ```scala myApp.provide(TRandom.live) ``` --- ## TReentrantLock A `TReentrantLock` allows safe concurrent access to some mutable state efficiently, allowing multiple fibers to read the state (because that is safe to do) but only one fiber to modify the state (to prevent data corruption). Also, even though the `TReentrantLock` is implemented using `STM`; reads and writes can be committed, allowing this to be used as a building block for solutions that expose purely ZIO effects and internally allow locking on more than one piece of state in a simple and composable way (thanks to STM). A `TReentrantLock` is a _reentrant_ read/write lock. A reentrant lock is one where a fiber can claim the lock multiple times without blocking on itself. It's useful in situations where it's not easy to keep track of whether you have already grabbed a lock. If a lock is non re-entrant you could grab the lock, then block when you go to grab it again, effectively causing a deadlock. ## Semantics This lock allows both readers and writers to reacquire read or write locks with reentrancy guarantees. Readers are not allowed until all write locks held by the writing fiber have been released. Writers are not allowed unless there are no other locks or the fiber wanting to hold a write lock already has a read lock and there are no other fibers holding a read lock. This lock also allows upgrading from a read lock to a write lock (automatically) and downgrading from a write lock to a read lock (automatically provided that you upgraded from a read lock to a write lock). ## Creating a reentrant lock ```scala val reentrantLock = TReentrantLock.make ``` ## Acquiring a read lock ```scala val program = (for { lock <- TReentrantLock.make _ <- lock.acquireRead rst <- lock.readLocked // lock is read-locked once transaction completes wst <- lock.writeLocked // lock is not write-locked } yield rst && !wst).commit ``` ## Acquiring a write lock ```scala val writeLockProgram: UIO[Boolean] = (for { lock <- TReentrantLock.make _ <- lock.acquireWrite wst <- lock.writeLocked // lock is write-locked once transaction completes rst <- lock.readLocked // lock is not read-locked } yield !rst && wst).commit ``` ## Multiple fibers can hold read locks ```scala val multipleReadLocksProgram: UIO[(Int, Int)] = for { lock <- TReentrantLock.make.commit fiber0 <- lock.acquireRead.commit.fork // fiber0 acquires a read-lock currentState1 <- fiber0.join // 1 read lock held fiber1 <- lock.acquireRead.commit.fork // fiber1 acquires a read-lock currentState2 <- fiber1.join // 2 read locks held } yield (currentState1, currentState2) ``` ## Upgrading and downgrading locks If your fiber already has a read lock then it is possible to upgrade the lock to a write lock provided that no other reader (other than your fiber) holds a lock ```scala val upgradeDowngradeProgram: UIO[(Boolean, Boolean, Boolean, Boolean)] = for { lock <- TReentrantLock.make.commit _ <- lock.acquireRead.commit _ <- lock.acquireWrite.commit // upgrade isWriteLocked <- lock.writeLocked.commit // now write-locked isReadLocked <- lock.readLocked.commit // and read-locked _ <- lock.releaseWrite.commit // downgrade isWriteLockedAfter <- lock.writeLocked.commit // no longer write-locked isReadLockedAfter <- lock.readLocked.commit // still read-locked } yield (isWriteLocked, isReadLocked, isWriteLockedAfter, isReadLockedAfter) ``` ## Acquiring a write lock in a contentious scenario A write lock can be acquired immediately only if one of the following conditions are satisfied: 1. There are no other holders of the lock 2. The current fiber is already holding a read lock and there are no other parties holding a read lock If either of the above scenarios are untrue then attempting to acquire a write lock will semantically block the fiber. Here is an example which demonstrates that a write lock can only be obtained by the fiber once all other readers (except the fiber attempting to acquire the write lock) have released their hold on the (read or write) lock. ```scala val writeLockDemoProgram: UIO[Unit] = for { l <- TReentrantLock.make.commit _ <- printLine("Beginning test").orDie f1 <- (l.acquireRead.commit *> ZIO.sleep(5.seconds) *> l.releaseRead.commit).fork f2 <- (l.acquireRead.commit *> printLine("read-lock").orDie *> l.acquireWrite.commit *> printLine("I have upgraded!").orDie).fork _ <- (f1 zip f2).join } yield () ``` Here fiber `f1` acquires a read lock and sleeps for 5 seconds before releasing it. Fiber `f2` also acquires a read lock and immediately tries to acquire a write lock. However, `f2` will have to semantically block for approximately 5 seconds to obtain a write lock because `f1` will release its hold on the lock and only then can `f2` acquire a hold for the write lock. ## Safer methods (`readLock` and `writeLock`) Using `acquireRead`, `acquireWrite`, `releaseRead` and `releaseWrite` should be avoided for simple use cases relying on methods like `readLock` and `writeLock` instead. `readLock` and `writeLock` automatically acquire and release the lock thanks to the `Scope` construct. The program described below is a safer version of the program above and ensures we don't hold onto any resources once we are done using the reentrant lock. ```scala val saferProgram: UIO[Unit] = for { lock <- TReentrantLock.make.commit f1 <- ZIO.scoped(lock.readLock *> ZIO.sleep(5.seconds) *> printLine("Powering down").orDie).fork f2 <- ZIO.scoped(lock.readLock *> lock.writeLock *> printLine("Huzzah, writes are mine").orDie).fork _ <- (f1 zip f2).join } yield () ``` --- ## TRef A `TRef[A]` is a mutable reference to an immutable value, which can participate in transactions in STM. The mutable reference can be retrieved and set from within transactions, with strong guarantees for atomicity, consistency, and isolation from other transactions. `TRef` provides the low-level machinery to create transactions from modifications of STM memory. ## Create a TRef Creating a `TRef` inside a transaction: ```scala val createTRef: STM[Nothing, TRef[Int]] = TRef.make(10) ``` Or creating a `TRef` inside a transaction, and immediately committing the transaction, which allows you to store and pass along the reference. ```scala val commitTRef: UIO[TRef[Int]] = TRef.makeCommit(10) ``` ## Retrieve the value out of a TRef Retrieving the value in a single transaction: ```scala val retrieveSingle: UIO[Int] = (for { tRef <- TRef.make(10) value <- tRef.get } yield value).commit ``` Or on multiple transactional statements: ```scala val retrieveMultiple: UIO[Int] = for { tRef <- TRef.makeCommit(10) value <- tRef.get.commit } yield value ``` ## Set a value to a TRef Setting the value overwrites the existing content of a reference. Setting the value in a single transaction: ```scala val setSingle: UIO[Int] = (for { tRef <- TRef.make(10) _ <- tRef.set(20) nValue <- tRef.get } yield nValue).commit ``` Or on multiple transactions: ```scala val setMultiple: UIO[Int] = for { tRef <- TRef.makeCommit(10) nValue <- tRef.set(20).flatMap(_ => tRef.get).commit } yield nValue ``` ## Update the value of the TRef The update function `A => A` allows computing a new value for the `TRef` using the old value. Updating the value in a single transaction: ```scala val updateSingle: UIO[Int] = (for { tRef <- TRef.make(10) nValue <- tRef.updateAndGet(_ + 20) } yield nValue).commit ``` Or on multiple transactions: ```scala val updateMultiple: UIO[Int] = for { tRef <- TRef.makeCommit(10) nValue <- tRef.updateAndGet(_ + 20).commit } yield nValue ``` ## Modify the value of the TRef The modify function `A => (B, A): B` works similar to `update`, but allows extracting some information (the `B`) out of the update operation. Modify the value in a single transaction: ```scala val modifySingle: UIO[(String, Int)] = (for { tRef <- TRef.make(10) mValue <- tRef.modify(v => ("Zee-Oh", v + 10)) nValue <- tRef.get } yield (mValue, nValue)).commit ``` Or on multiple transactions: ```scala val modifyMultiple: UIO[(String, Int)] = for { tRef <- TRef.makeCommit(10) tuple2 <- tRef.modify(v => ("Zee-Oh", v + 10)).zip(tRef.get).commit } yield tuple2 ``` ## Example usage Here is a scenario where we use a `TRef` to hand-off a value between two `Fiber`s ```scala def transfer(tSender: TRef[Int], tReceiver: TRef[Int], amount: Int): UIO[Int] = { STM.atomically { for { _ <- tSender.get.retryUntil(_ >= amount) _ <- tSender.update(_ - amount) nAmount <- tReceiver.updateAndGet(_ + amount) } yield nAmount } } val transferredMoney: UIO[String] = for { tSender <- TRef.makeCommit(50) tReceiver <- TRef.makeCommit(100) _ <- transfer(tSender, tReceiver, 50).fork _ <- tSender.get.retryUntil(_ == 0).commit tuple2 <- tSender.get.zip(tReceiver.get).commit (senderBalance, receiverBalance) = tuple2 } yield s"sender: $senderBalance & receiver: $receiverBalance" ``` In this example, we create and commit two transactional references for the sender and receiver to be able to extract their value. On the following step, we create an atomic transactional that updates both accounts only when there is sufficient balance available in the sender account. In the end, we fork to run asynchronously. On the running fiber, we suspend until the sender balance suffers changes, in this case, to reach `zero`. Finally, we extract the new values out of the accounts and combine them in one result. --- ## TSemaphore `TSemaphore` is a semaphore with transactional semantics that can be used to control access to a common resource. It holds a certain number of permits, and permits may be acquired or released. ## Create a TSemaphore Creating a `TSemaphore` with 10 permits: ```scala val tSemaphoreCreate: STM[Nothing, TSemaphore] = TSemaphore.make(10L) ``` ## Acquire a permit Acquiring a permit reduces the number of remaining permits that the `TSemaphore` contains. Acquiring a permit is done when a user wants to access a common shared resource: ```scala val tSemaphoreAcq: STM[Nothing, TSemaphore] = for { tSem <- TSemaphore.make(2L) _ <- tSem.acquire } yield tSem tSemaphoreAcq.commit ``` Note that if you try to acquire a permit when there are no more remaining permits in the semaphore then execution will be blocked semantically until a permit is ready to be acquired. Note that semantic blocking does not block threads and the STM transaction will only be retried when a permit is released. ## Release a permit Once you have finished accessing the shared resource, you must release your permit so other parties can access the shared resource: ```scala val tSemaphoreRelease: STM[Nothing, TSemaphore] = for { tSem <- TSemaphore.make(1L) _ <- tSem.acquire _ <- tSem.release } yield tSem tSemaphoreRelease.commit ``` ## Retrieve available permits You can query for the remaining amount of permits in the TSemaphore by using `available`: ```scala val tSemaphoreAvailable: STM[Nothing, Long] = for { tSem <- TSemaphore.make(2L) _ <- tSem.acquire cap <- tSem.available } yield cap tSemaphoreAvailable.commit ``` The above code creates a TSemaphore with two permits and acquires one permit without releasing it. Here, `available` will report that there is a single permit left. ## Execute an arbitrary STM action with automatic acquire and release You can choose to execute any arbitrary STM action that requires acquiring and releasing permit on TSemaphore as part of the same transaction. Rather than doing: ```scala def yourSTMAction: STM[Nothing, Unit] = STM.unit val tSemaphoreWithoutPermit: STM[Nothing, Unit] = for { sem <- TSemaphore.make(1L) _ <- sem.acquire a <- yourSTMAction _ <- sem.release } yield a tSemaphoreWithoutPermit.commit ``` You can simply use `withPermit` instead: ```scala val tSemaphoreWithPermit: IO[Nothing, Unit] = for { sem <- TSemaphore.make(1L).commit a <- sem.withPermit(yourSTMAction.commit) } yield a ``` It is considered best practice to use `withPermit` over using an `acquire` and a `release` directly unless dealing with more complicated use cases that involve multiple STM actions where `acquire` is not at the start and `release` is not at the end of the STM transaction. ## Acquire and release multiple permits It is possible to acquire and release multiple permits at a time using `acquireN` and `releaseN`: ```scala val tSemaphoreAcquireNReleaseN: STM[Nothing, Boolean] = for { sem <- TSemaphore.make(3L) _ <- sem.acquireN(3L) cap <- sem.available _ <- sem.releaseN(3L) } yield cap == 0 tSemaphoreAcquireNReleaseN.commit ``` --- ## TSet A `TSet[A]` is a mutable set that can participate in transactions in STM. ## Create a TSet Creating an empty `TSet`: ```scala val emptyTSet: STM[Nothing, TSet[Int]] = TSet.empty[Int] ``` Or creating a `TSet` with specified values: ```scala val specifiedValuesTSet: STM[Nothing, TSet[Int]] = TSet.make(1, 2, 3) ``` Alternatively, you can create a `TSet` by providing a collection of values: ```scala val iterableTSet: STM[Nothing, TSet[Int]] = TSet.fromIterable(List(1, 2, 3)) ``` In case there are duplicates provided, the last one is taken. ## Put an element to a TSet The new element can be added to the set in the following way: ```scala val putElem: UIO[TSet[Int]] = (for { tSet <- TSet.make(1, 2) _ <- tSet.put(3) } yield tSet).commit ``` In case the set already contains the element, no modification will happen. ## Remove an element from a TSet The simplest way to remove an element from `TSet` is using `delete` method: ```scala val deleteElem: UIO[TSet[Int]] = (for { tSet <- TSet.make(1, 2, 3) _ <- tSet.delete(1) } yield tSet).commit ``` Also, it is possible to remove every element that satisfies provided predicate: ```scala val removedEvenElems: UIO[TSet[Int]] = (for { tSet <- TSet.make(1, 2, 3, 4) _ <- tSet.removeIf(_ % 2 == 0) } yield tSet).commit ``` Or you can keep all the elements that match predicate function: ```scala val retainedEvenElems: UIO[TSet[Int]] = (for { tSet <- TSet.make(1, 2, 3, 4) _ <- tSet.retainIf(_ % 2 == 0) } yield tSet).commit ``` Note that `retainIf` and `removeIf` serve the same purpose as `filter` and `filterNot`. The reason for naming them differently was to emphasize a distinction in their nature. Namely, both `retainIf` and `removeIf` are destructive - calling them can modify the collection. ## Union of a TSet Union of the sets A and B represents the set of elements belonging to set A or set B, or both. Using `A union B` method modifies set `A`. ```scala // unionTSet = {1, 2, 3, 4, 5, 6} val unionTSet: UIO[TSet[Int]] = (for { tSetA <- TSet.make(1, 2, 3, 4) tSetB <- TSet.make(3, 4, 5, 6) _ <- tSetA.union(tSetB) } yield tSetA).commit ``` ## Intersection of a TSet The intersection of the sets A and B is the set of elements belonging to both A and B. Using `A intersect B` method modifies set `A`. ```scala // intersectionTSet = {3, 4} val intersectionTSet: UIO[TSet[Int]] = (for { tSetA <- TSet.make(1, 2, 3, 4) tSetB <- TSet.make(3, 4, 5, 6) _ <- tSetA.intersect(tSetB) } yield tSetA).commit ``` ## Difference of a TSet The difference between sets A and B is the set containing elements of set A but not in B. Using `A diff B` method modifies set `A`. ```scala // diffTSet = {1, 2} val diffTSet: UIO[TSet[Int]] = (for { tSetA <- TSet.make(1, 2, 3, 4) tSetB <- TSet.make(3, 4, 5, 6) _ <- tSetA.diff(tSetB) } yield tSetA).commit ``` ## Transform elements of a TSet The transform function `A => A` allows computing a new value for every element in the set: ```scala val transformTSet: UIO[TSet[Int]] = (for { tSet <- TSet.make(1, 2, 3, 4) _ <- tSet.transform(a => a * a) } yield tSet).commit ``` Note that it is possible to shrink a `TSet`: ```scala val shrinkTSet: UIO[TSet[Int]] = (for { tSet <- TSet.make(1, 2, 3, 4) _ <- tSet.transform(_ => 1) } yield tSet).commit ``` Resulting set in example above has only one element. Note that `transform` serves the same purpose as `map`. The reason for naming it differently was to emphasize a distinction in its nature. Namely, `transform` is destructive - calling it can modify the collection. The elements can be mapped effectfully via `transformSTM`: ```scala val transformSTMTSet: UIO[TSet[Int]] = (for { tSet <- TSet.make(1, 2, 3, 4) _ <- tSet.transformSTM(a => STM.succeed(a * a)) } yield tSet).commit ``` Folds the elements of a `TSet` using the specified associative binary operator: ```scala val foldTSet: UIO[Int] = (for { tSet <- TSet.make(1, 2, 3, 4) sum <- tSet.fold(0)(_ + _) } yield sum).commit ``` The elements can be folded effectfully via `foldSTM`: ```scala val foldSTMTSet: UIO[Int] = (for { tSet <- TSet.make(1, 2, 3, 4) sum <- tSet.foldSTM(0)((acc, el) => STM.succeed(acc + el)) } yield sum).commit ``` ## Perform effects for TSet elements `foreach` is used for performing an STM effect for each element in set: ```scala val foreachTSet = (for { tSet <- TSet.make(1, 2, 3, 4) tQueue <- TQueue.unbounded[Int] _ <- tSet.foreach(a => tQueue.offer(a).unit) } yield tSet).commit ``` ## Check TSet membership Checking whether the element is present in a `TSet`: ```scala val tSetContainsElem: UIO[Boolean] = (for { tSet <- TSet.make(1, 2, 3, 4) res <- tSet.contains(3) } yield res).commit ``` ## Convert TSet to a List List of set elements can be obtained as follows: ```scala val tSetToList: UIO[List[Int]] = (for { tSet <- TSet.make(1, 2, 3, 4) list <- tSet.toList } yield list).commit ``` ## Size of a TSet Set's size can be obtained as follows: ```scala val tSetSize: UIO[Int] = (for { tSet <- TSet.make(1, 2, 3, 4) size <- tSet.size } yield size).commit ``` --- ## Chunk A `Chunk[A]` represents a chunk of values of type `A`. Chunks are usually backed by arrays, but expose a purely functional, safe interface to the underlying elements, and they become lazy on operations that would be costly with arrays, such as repeated concatenation. Like lists and arrays, Chunk is an ordered collection. ## Why Chunk? Arrays are fast and don’t box primitive values but due to `ClassTag` requirements and mutability they are painful to use and don't integrate well into functional code. ZIO chunks are backed by arrays so they also have zero boxing for primitives while providing an immutable interface and avoiding `ClassTag` requirements. Let's get more details behind why Chunk was invented: ### Immutability In Scala, there is no immutable data type that can efficiently represent primitive data types. There is Array, but Array is a mutable interface. The Array data type can efficiently represent primitives without boxing, but only by exposing some unsafe mutable methods like `update`. ### Ergonomic Design Every time, when we create an array of generic types in Scala, we need a [ClassTag](https://www.scala-lang.org/api/current/scala/reflect/ClassTag.html) to provide runtime information about that generic type, which is very inconvenient and isn't ergonomic. It leads us to a very cumbersome API. Chunk does not have the inconvenience of Array in Scala. **Chunk dispenses with the need to have ClassTags**. It utilizes a different approach to solve that problem. ### High Performance In addition to being an immutable array and zero boxing of Chunks that leads us to a high performant data type, Chunk has specialized operations for things like appending a single element or concatenating two Chunks together which have significantly higher performance than doing these same operations on the Array. Many Chunk methods have been handwritten to achieve better performance than their corresponding Array implementations in the Scala standard library. Although Chunk is a common data type in ZIO, it exists primarily to support streaming use cases. When we are doing data streaming, a lot of times the source stream is a stream of bytes. Hence, internally we use a Chunk of bytes to represent that, so we don't have to box the bytes. Of course, it can be utilized for Chunks of Ints and many other types. Using Chunk is especially common when we are encoding and decoding at the level of streams. It is a very efficient, high-performance data type. ## Operations ### Creating a Chunk Creating empty `Chunk`: ``` val emptyChunk = Chunk.empty ``` Creating a `Chunk` with specified values: ```scala val specifiedValuesChunk = Chunk(1,2,3) // specifiedValuesChunk: Chunk[Int] = IndexedSeq(1, 2, 3) ``` Alternatively, we can create a `Chunk` by providing a collection of values: ```scala val fromIterableChunk: Chunk[Int] = Chunk.fromIterable(List(1, 2, 3)) // fromIterableChunk: Chunk[Int] = IndexedSeq(1, 2, 3) val fromArrayChunk: Chunk[Int] = Chunk.fromArray(Array(1, 2, 3)) // fromArrayChunk: Chunk[Int] = IndexedSeq(1, 2, 3) ``` Creating a `Chunk` using filling same n element into it: ```scala val chunk: Chunk[Int] = Chunk.fill(3)(0) // chunk: Chunk[Int] = IndexedSeq(0, 0, 0) ``` Creating a `Chunk` using unfold method by repeatedly applying the given function, as long as it returns Some: ```scala val unfolded = Chunk.unfold(0)(n => if (n < 8) Some((n*2, n+2)) else None) // unfolded: Chunk[Int] = IndexedSeq(0, 4, 8, 12) ``` ### Concatenating chunk `++` Returns the concatenation of this chunk with the specified chunk. For example: ```scala Chunk(1,2,3) ++ Chunk(4,5,6) // res0: Chunk[Int] = IndexedSeq(1, 2, 3, 4, 5, 6) ``` ### Collecting chunk `collect` Returns a filtered, mapped subset of the elements of this chunk. How to use `collect` function to cherry-pick all strings from Chunk[A]: ```scala val collectChunk = Chunk("Hello ZIO", 1.5, "Hello ZIO NIO", 2.0, "Some string", 2.5) // collectChunk: Chunk[Any] = IndexedSeq( // "Hello ZIO", // 1.5, // "Hello ZIO NIO", // 2.0, // "Some string", // 2.5 // ) collectChunk.collect { case string: String => string } // res1: Chunk[String] = IndexedSeq( // "Hello ZIO", // "Hello ZIO NIO", // "Some string" // ) ``` How to use `collect` function to cherry-pick all the digits from Chunk[A]: ```scala collectChunk.collect { case digit: Double => digit } // res2: Chunk[Double] = IndexedSeq(1.5, 2.0, 2.5) ``` `collectWhile` collects the elements (from left to right) until the predicate returns "false" for the first time: ```scala Chunk("Sarah", "Bob", "Jane").collectWhile { case element if element != "Bob" => true } // res3: Chunk[Boolean] = IndexedSeq(true) ``` or another example: ```scala Chunk(9, 2, 5, 1, 6).collectWhile { case element if element >= 2 => true } // res4: Chunk[Boolean] = IndexedSeq(true, true, true) ``` ### Dropping chunk `drop` drops the first `n` elements of the chunk: ```scala Chunk("Sarah", "Bob", "Jane").drop(1) // res5: Chunk[String] = IndexedSeq("Bob", "Jane") ``` `dropWhile` drops all elements so long as the predicate returns true: ```scala Chunk(9, 2, 5, 1, 6).dropWhile(_ >= 2) // res6: Chunk[Int] = IndexedSeq(1, 6) ``` ### Comparing chunks ```scala Chunk("A","B") == Chunk("A", "C") // res7: Boolean = false ``` ### Converting chunks `toArray` converts the chunk into an Array. ```scala Chunk(1,2,3).toArray ``` `toSeq` converts the chunk into `Seq`. ``` scala mdoc Chunk(1,2,3).toSeq ``` --- ## Introduction to ZIO Streams The primary goal of a streaming library is to introduce **a high-level API that abstracts the mechanism of reading and writing operations using data sources and destinations**. A streaming library helps us to concentrate on the business logic and separates us from low-level implementation details. ## Use Cases There are lots of examples of streaming that people might not recognize, this is a common problem especially for beginners. A beginner might say "I don't need a streaming library. Why should I use that?". It's because they don't see streams. Once we use a streaming library, we start to see streams everywhere but until then we don't understand where they are. Before diving into ZIO Streams, let's list some use cases of a streaming solution and see why we would want to program in a streaming fashion: - **Files** — Every time an old school API interacting with a file has very low-level operators like "Open a file, get me an InputStream, and a method to read the next chunk from that InputStream, and also another method to close the file". Although that is a very low-level imperative API, there is a way to see files as streams of bytes. - **Sockets** — Instead of working with low-level APIs, we can use streams to provide a stream-based implementation of server socket that hides the low-level implementation details of sockets. We could model socket communication as a function from a stream of bytes to a stream of bytes. We can view the input of that socket as being a stream, and its output as being another stream. - **Event-Sourcing** — In these days and age, it is common to want to build event-sourced applications which work on events or messages in a queuing system like Kafka or AMQP systems and so forth. The foundation of this architecture is streaming. Also, they are useful when we want to do a lot of data analytics and so forth. - **UI Applications** — Streams are the foundation of almost every single modern UI application. Every time we click on something, under the hood that is an event. We can use low-level APIs like subscribing callbacks to the user events but also we can view those as streams of events. So we can model subscriptions as streams of events in UI applications. - **HTTP Server** — An HTTP Server can be viewed as a stream. We have a stream of requests that are being transformed to a stream of responses; a function from a stream of bytes that go to a stream of bytes. So streams are everywhere. We can see all of these different things as being streams. Everywhere we look we can find streams. Basically, all data-driven applications, almost all data-driven applications can benefit from streams. ## Motivation Assume, we would like to take a list of numbers and grab all the prime numbers and then do some more hard work on each of these prime numbers. We can do it using `ZIO.foreachPar` and `ZIO.filterPar` operators like this: ```scala def isPrime(number: Int): Task[Boolean] = ZIO.succeed(???) def moreHardWork(i: Int): Task[Boolean] = ZIO.succeed(???) val numbers = 1 to 1000 for { primes <- ZIO.filterPar(numbers)(isPrime) _ <- ZIO.foreachPar(primes)(moreHardWork) @@ parallel(20) } yield () ``` This processes the list in parallel and filters all the prime numbers, then takes all the prime numbers and does some more hard work on them. There are two problems with this example: - **High Latency** — We are not getting any pipelining, we are doing batch processing. We need to wait for the entire list to be processed in the first step before we can continue to the next step. This can lead to a pretty severe loss of performance. - **Limited Memory** — We need to keep the entire list in memory as we process it and this doesn't work if we are working with an infinite data stream. With ZIO stream we can change this program to the following code: ```scala def prime(number: Int): Task[(Boolean, Int)] = ZIO.succeed(???) ZStream.fromIterable(numbers) .mapZIOParUnordered(20)(prime(_)) .filter(_._1).map(_._2) .mapZIOParUnordered(20)(moreHardWork(_)) ``` We converted the list of numbers using `ZStream.fromIterable` into a `ZStream`, then we mapped it in parallel, twenty items at a time, and then we performed the hard work problem, twenty items of a time. This is a pipeline, and this easily works for an infinite list. One might ask, "Okay, I can get the pipelining by using fibers and queues. So why should I use ZIO streams?". It is extremely tempting to write up the pipeline look like this. We can create a bunch of queues and fibers, then we have fibers that copy information between the queues and perform the processing concurrently. It ends up something like this: ```scala def writeToInput(q: Queue[Int]): Task[Unit] = ZIO.succeed(???) def processBetweenQueues(from: Queue[Int], to: Queue[Int]): Task[Unit] = ZIO.succeed(???) def printElements(q: Queue[Int]): Task[Unit] = ZIO.succeed(???) for { input <- Queue.bounded[Int](16) middle <- Queue.bounded[Int](16) output <- Queue.bounded[Int](16) _ <- writeToInput(input).fork _ <- processBetweenQueues(input, middle).fork _ <- processBetweenQueues(middle, output).fork _ <- printElements(output).fork } yield () ``` We created a bunch of queues for buffering source, destination elements, and intermediate results. There are some problems with this solution. As fibers are low-level concurrency tools, using them to create a data pipeline is not straightforward. We need to handle interruptions properly. We should care about resources and prevent them to leak. We need to shutdown the pipeline in a right way by waiting for queues to be drained. Although fibers are very efficient and more performant than threads. They are advanced concurrency tools. So it is better to avoid using them to do manual pipelining. Instead, we can use ZIO streams: ```scala def generateElement: Task[Int] = ZIO.succeed(???) def process(i: Int): Task[Int] = ZIO.succeed(???) def printElem(i: Int): Task[Unit] = ZIO.succeed(???) ZStream .repeatZIO(generateElement) .buffer(16) .mapZIO(process(_)) .buffer(16) .mapZIO(process(_)) .buffer(16) .tap(printElem(_)) ``` We have a buffer in between each step. We performed our computations in between. This is everything we need to get that pipelining in the same fashion that it looked before. ## Why Streams? ZIO stream has super compelling advantages of using high-level streams. ZIO solution to streaming solves a lot of common streaming pain points. It shines in the following topics: ### 1. High-level and Declarative This means in a very short snippet of a fluent code we can solve very outrageously complicated problems with just a few simple lines. ### 2. Asynchronous and Non-blocking They're reactive streams, they don't block threads. They're super-efficient and very scalable. We can minimize our application latency and increase its performance. We can avoid wasting precious thread resources by using non-blocking and asynchronous ZIO streams. ### 3. Concurrency and Parallelism Streams are concurrent. They have a lot of concurrent operators. All the operations on them are safe to use in presence of concurrency. And also just like ZIO gives us parallel operators with everything, there are lots of parallel operators. We can use the parallel version of operators, like `mapZIOPar`, `flatMapPar`. Parallel operators allow us to fully saturate and utilize all CPU cores of our machine. If we need to do bulk processing on a lot of data and use all the cores on our machine, so we can speed up the process by using these parallel operators. ### 4. Resource Safety Resource safety is not a simple thing to guarantee. Assume when we have several streams, some of them are sockets and files, some of them are API calls and database queries. When we have all these streams, and we are transforming and combining them, and we are timing some out, and also some of them are doing concurrent merges; what happens when things go wrong in one part of that stream graph? ZIO streams provides the guarantee that it will never leak resources. So when streams have to be terminated for error or timeout or interruption reasons or whatever, ZIO will always safely shutdown and release the resources associated with that stream usage. We don't have to worry about resource management anymore. We can work at high-level and just declaratively describe our stream graph and then ZIO will handle the tricky job of executing that and taking care to make sure that no resources are leaked in an event of something bad happens or even just a timeout, or interruption, or just we are done with a result. So resources are always safely released without any leaks. ### 5. High Performance and Efficiency When we are doing an I/O job, the granularity of data is not at the level of a single byte. For example, we never read or write a single element from/to a file descriptor. We always use multiple elements. So when we are doing an I/O operation it is a poor practice to read/write element by element and this decreases the performance of our program. In order to achieve high efficiency, ZIO stream implicitly chunks everything, but it still presents us with a nice API that is at the level of every single element. So we can always deal with streams of individual elements even though behind-the-scenes ZIO is doing some chunking to make that performant. This is one of the tricks that enables ZIO streams to have such great performance. ZIO Streams are working at the level of chunks. Every time we are working with ZIO streams, we are also working with chunks implicitly. So there are no streams with individual elements. Streams always use chunks. Every time we pull an element out of a ZIO stream, we end up pulling a chunk of elements under the hood. ### 6. Seamless Integration with ZIO ZIO stream has a powerful seamless integrated support for ZIO. It uses `Scope`, `Schedule`, and any other powerful data types in ZIO. So we can stay within the same ecosystem and get all its significant benefits. ### 7. Back-Pressure We get back-pressuring for free. With ZIO streams it is actually not a back-pressuring, but it is equivalent. In push-based streams like Akka Streams, streams are push-based; when an element comes in, it is pushed downward in the pipeline. That is what leads to the need for back-pressuring. Back-pressuring makes the push-based stream much more complicated than it needs to be. Push-based streams are good at splitting streams because we have one element, and we can push it to two different places. That is nice and elegant, but they're terrible at merging streams and that is because you end up needing to use queues, and then we run into a problem. In the case of using queues, we need back-pressuring, which leads to a complicated architecture. In ZIO when we merge streams, ZIO uses pull-based streams. They need minimal computation because we pull elements at the end of our data pipeline when needed. When the sink asks for one element, then that ripples all the way back through the very edges of the system. So when we pull one element at the end, no additional computation takes place until we pull the next element or decide that we are done pulling, and we close the stream. It causes the minimum amount of computation necessary to produce the result. Using the pull-based mechanism we have no producers, and it prevents producing more events than necessary. So ZIO streams does not need back-pressure even though it provides a form of that because it is lazy and on-demand and uses pull-based streams. So ZIO stream gives us the benefits of back-pressuring, but in a cleaner conceptual model that is very efficient and does not require all these levels of buffering. ### 8. Infinite Data using Finite Memory Streams let us work on infinite data in a finite amount of memory. When we are writing streaming logic, we don't have to worry about how much data we are ultimately going to be processed. That is because we are just building a workflow, a description of the processing. We are not manually loading up everything into memory, into a list, and then doing our processing on a list. That doesn't work very well because we can only fit a finite amount of memory into our computer at one time. ZIO streams enable us just concentrate on our business problem, and not on how much memory this program is going to consume. So we can write these computations that work over streams that are totally infinite but in a finite amount of memory and ZIO handles that for us. Assume we have the following code. This is a snippet of a code that reads a file into a string and splits the string into new lines, then iterates over lines and prints them out. It is pretty simple and easy to read and also it is simple to understand: ```scala for (line <- FileUtils.readFileToString(new File("file.txt")).split('\n')) println(line) ``` The only problem here is that if we run this code with a file that is very large which is bigger than our memory, that is not going to work. Instead, we can reach the same functionality, by using the stream API: ```scala ZStream.fromFileName("file.txt") .via(ZPipeline.utf8Decode >>> ZPipeline.splitLines) .foreach(printLine(_)) ``` By using ZIO streams, we do not care how big is a file, we just concentrate on the logic of our application. ## Core Abstractions To define a stream workflow there are three core abstraction in ZIO stream; _Streams_, _Sinks_, and _Pipelines_: 1. **[ZStream](zstream/index.md)** — Streams act as _sources_ of values. We get elements from them. They produce values. 2. **[ZSink](zsink/index.md)** — Sinks act as _receptacles_ or _sinks_ for values. They consume values. 3. **[ZPipeline](zpipeline.md)** — Pipelines act as _transformers_ of values. They take individual values, and they transform or decode them. ### Stream The `ZStream` data type similar to the `ZIO` effect has `R`, `E`, and `A`. It has environment, error, and element type. The difference between the `ZIO` and `ZStream` is that: - A `ZIO` effect will always succeed or fail. If it succeeds, it will succeed with a single element. - A `ZStream` can succeed with zero or more elements. So we can have an _empty stream_. A `ZStream[R, E, A]` doesn't necessarily produce any `A`s, it produces zero or more `A`s. So, that is a big difference. There is no such thing as a non-empty `ZStream`. All `ZStreams` are empty, they can produce any number of `A`s, which could be an infinite number of `A`s. There is no way to check to see if a stream is empty or not, because that computation hasn't started. Streams are super lazy, so there is no way to say "Oh! does this stream contain anything?" No! We can't figure that out. We have to use it and try to do something with it, and then we are going to figure out whether it had something. ### Sink The basic idea behind the `Sink` is that **it consumes values of some type, and then it ends up when it is done. When the sink is done, it produces the value of a different type**. Sinks are a bit like **parsers**; they consume some input, when they're done, they produce a value. Also, they are like **databases**; they read enough from input when they don't want anymore, they can produce some value or return unit. Some sinks will produce nothing as their return type parameter is `Nothing`, which means that the sink is always going to accept more and more input; it is never ever going to be done. Just like Streams, sinks are super compositional. Sink's operators allow us to combine two sinks together or transform them. That allows us to generate a vast variety of sinks. Streams and Sinks are duals in category theory. One produces values, and the other one consumes them. They are mere images of each other. They both have to exist. A streaming library cannot be complete unless it has streams and sinks. That is why ZIO has a sort of better design than FS2 because FS2 has a stream, but it doesn't have a sink. Its Sink is just faked. It doesn't actually have a real sink. ZIO has a real sink, and we can compose them to generate new sinks. ### Pipeline With `Pipeline`s, we can transform streams from one type to another, in a **stateful fashion**, which is sometimes necessary when we are doing encoding and decoding. Pipeline is a transformer of element types. Pipeline accepts some element of type `A` and produces some element of type `B`, and it may fail along the way or use the environment. It just transforms elements from one type to another type in a stateful way. For example, we can write counter with pipelines. We take strings and then split them into lines, and then we take the lines, and we split them into words, and then we count them. Another common use case of pipelines is **writing codecs**. We can use them to decode the bytes into strings. We have a bunch of bytes, and we want to end up with a JSON and then once we are in JSON land we want to go from JSON to our user-defined data type. So, by writing a pipeline we can convert that JSON to our user-defined data type. Pipelines can be thought of as **element transformers**. They transform elements of a stream: 1. We can take a pipeline, and we can stack it onto a stream to change the element type. For example, we have a Stream of `A`s, and a pipeline that goes from `A` to `B`, so we can take that pipeline from `A` to `B` and stack it on the stream to get back a stream of `B`s. 2. Also, we can stack a pipeline onto the front of a sink to change the input element type. If some sink consumes `B`s, and we have a pipeline from `A` to `B` we can take that pipeline stack it onto the front of the sink and get back a new sink that consumes `A`s. Assume we are building the data pipeline, the elements come from the far left, and they end up on the far right. Events come from the stream, they end up on the sink, along the way they're transformed by pipelines. **Pipelines are the middle section of the pipe that keep on transforming those elements in a stateful way**. --- ## Installing ZIO Streams In order to use ZIO Streaming, we need to add the required configuration in our SBT settings: ```scala libraryDependencies += Seq( "dev.zio" %% "zio" % "2.1.26" % Test "dev.zio" %% "zio-streams" % "2.1.26" % Test ) ``` --- ## SubscriptionRef A `SubscriptionRef[A]` is a `Ref` that lets us subscribe to receive the current value along with all changes to that value. ```scala trait SubscriptionRef[A] extends Ref.Synchronized[A] { def changes: ZStream[Any, Nothing, A] } ``` We can use all the normal methods on `Ref.Synchronized` to `get`, `set`, or `modify` the current value. The `changes` stream can be consumed to observe the current value as well as all changes to that value. Since `changes` is just a description of a stream, each time we run the stream we will observe the current value as of that point in time as well as all changes after that. To create a `SubscriptionRef` you can use the `make` constructor, which makes a new `SubscriptionRef` with the specified initial value. ```scala object SubscriptionRef { def make[A](a: A): UIO[SubscriptionRef[A]] = ??? } ``` A `SubscriptionRef` can be extremely useful to model some shared state where one or more observers must perform some action for all changes in that shared state. For example, in a functional reactive programming context the value of the `SubscriptionRef` might represent one part of the application state and each observer would need to update various user interface elements based on changes in that state. To see how this works, let's create a simple example where a "server" repeatedly updates a value that is observed by multiple "clients". ```scala def server(ref: Ref[Long]): UIO[Nothing] = ref.update(_ + 1).forever ``` Notice that `server` just takes a `Ref` and does not need to know anything about `SubscriptionRef`. From its perspective it is just updating a value. ```scala def client(changes: ZStream[Any, Nothing, Long]): UIO[Chunk[Long]] = for { n <- Random.nextLongBetween(1, 200) chunk <- changes.take(n).runCollect } yield chunk ``` Similarly `client` just takes a `ZStream` of values and does not have to know anything about the source of these values. In this case we will simply observe a fixed number of values. To wire everything together, we start the server, then start multiple instances of the client in parallel, and finally shut down the server when we are done. We also actually create the `SubscriptionRef` here. ```scala for { subscriptionRef <- SubscriptionRef.make(0L) server <- server(subscriptionRef).fork chunks <- ZIO.collectAllPar(List.fill(100)(client(subscriptionRef.changes))) _ <- server.interrupt _ <- ZIO.foreach(chunks)(chunk => Console.printLine(chunk)) } yield () ``` This will ensure that each client observes the current value when it starts and all changes to the value after that. Since the changes are just streams it is also easy to build much more complex programs using all the stream operators we are accustomed to. For example, we can transform these streams, filter them, or merge them with other streams. --- ## Channel Interruption We can interrupt a channel using the `ZChannel.interruptWhen` operator. It takes a ZIO effect that will be evaluated, if it finishes before the channel is closed, it will interrupt the channel, and the terminal value of the returned channel will be the success value of the effect: ```scala def randomNumbers: ZChannel[Any, Any, Any, Any, Nothing, Int, Nothing] = ZChannel .fromZIO(Random.nextIntBounded(100)) .flatMap(ZChannel.write) *> ZChannel.fromZIO(ZIO.sleep(1.second)) *> randomNumbers randomNumbers.interruptWhen(ZIO.sleep(3.seconds).as("Done!")).runCollect.debug // One output: (Chunk(84,57,70),Done!) ``` Another version of `interruptWhen` takes a `Promise` as an argument. It will interrupt the channel when the promise is fulfilled: ```scala for { p <- Promise.make[Nothing, Unit] f <- randomNumbers .interruptWhen(p) .mapOutZIO(e => Console.printLine(e)) .runDrain .fork _ <- p.succeed(()).delay(5.seconds) _ <- f.join } yield () // Output: // 74 // 60 // 52 // 52 // 79 ``` --- ## Channel Operations ## Piping The values from the output port of the first channel are passed to the input port of the second channel when we pipe a channel to another channel: ```scala (ZChannel.writeAll(1,2,3) >>> (ZChannel.read[Int] <*> ZChannel.read[Int])).runCollect.debug // Output: (Chunk(),(1,2)) ``` ## Sequencing In order to sequence channels, we can use the `ZChannel#flatMap` operator. When we use the `flatMap` operator, we have the ability to chain two channels together. After the first channel is finished, we can create a new channel based on the terminal value of the first channel: ```scala ZChannel .fromZIO( Console.readLine("Please enter a number: ").map(_.toInt) ) .flatMap { case n if n < 0 => ZChannel.fail("Number must be positive") case n => ZChannel.writeAll((0 to n): _*) } .runCollect .debug // Sample Output: // Please enter a number: 5 // (Chunk(0,1,2,3,4,5),()) ``` ## Concatenating Suppose there is a channel that creates a new channel for each element of the outer channel and emits them to the output port. We can use `concatOut` to concatenate all the inner channels into a single channel: ```scala ZChannel .writeAll("a", "b", "c") .mapOut { l => ZChannel.writeAll((1 to 3).map(i => s"$l$i"):_*) } .concatOut .runCollect .debug // Output: (Chunk(a1,a2,a3,b1,b2,b3,c1,c2,c3),()) ``` We can do the same with `ZChannel.concatAll`: ```scala ZChannel .concatAll( ZChannel .writeAll("a", "b", "c") .mapOut { l => ZChannel.writeAll((1 to 3).map(i => s"$l$i"): _*) } ) .runCollect .debug // Output: (Chunk(a1,a2,a3,b1,b2,b3,c1,c2,c3),()) ``` ## Zipping We have two categories of `zip` operators: ordinary `zipXYZ` operators which run sequentially, and parallel `zipXYZ` operators which run in parallel. 1. `zip`/`<*>` operator: ```scala val first = ZChannel.write(1,2,3) *> ZChannel.succeed("Done!") val second = ZChannel.write(4,5,6) *> ZChannel.succeed("Bye!") (first <*> second).runCollect.debug // Output: (Chunk((1,2,3),(4,5,6)),(Done!,Bye!)) ``` 2. `zipRight`/`*>` operator: ```scala (first *> second).runCollect.debug ``` 3. `zipLeft`/`<*` operator: ```scala (first <* second).runCollect.debug ``` ## Mapping ### Mapping The Terminal Done Value (`OutDone`) The ordinary `map` operator is used to map the done value of a channel: ```scala ZChannel.writeAll(1, 2, 3).map(_ => 5).runCollect.debug // (Chunk(1,2,3),5) ``` ### Mapping The Done Value of The Input Port (`InDone`) To map the done value of the input port, we use the `contramap` operator: ```scala (ZChannel.succeed("5") >>> ZChannel .readWith( (i: Int) => ZChannel.write(ZChannel.write(i)), (_: Any) => ZChannel.unit, (d: Int) => ZChannel.succeed(d * 2) ) .contramap[String](_.toInt)).runCollect.debug // Output: (Chunk(),(10)) ``` ### Mapping The Error Value of The Output Port (`OutErr`) To map the failure value of a channel, we use the `mapError` operator: ```scala val channel = ZChannel .fromZIO(Console.readLine("Please enter you name: ")) .mapError(_.toString) ``` ### Mapping The Output Elements of a Channel (`OutElem`) To map the output elements of a channel, we use the `mapOutput` operator: ```scala ZChannel.writeAll(1,2,3).mapOut(_ * 2).runCollect.debug // Output: (Chunk(2,4,6),()) ``` ### Mapping The Input Elements of a Channel (`InElem`) To map the input elements of a channel, we use the `contramapIn` operator: ```scala (ZChannel.write("123") >>> ZChannel.read[Int].contramapIn[String](_.toInt * 2)).runCollect.debug // Output: (Chunk(),(246)) ``` ## Merging Merge operators are used to merging multiple channels into a single channel. They are used to combine the output port of channels concurrently. Every time any of the channels produces a value, the output port of the resulting channel will produce a value. Assume we have the following channel: ```scala def iterate( from: Int, to: Int ): ZChannel[Any, Any, Any, Any, Nothing, Int, Unit] = if (from <= to) ZChannel.write(from) *> ZChannel.fromZIO( Random .nextLongBounded(1000) .flatMap(delay => ZIO.sleep(Duration.fromMillis(delay))) ) *> iterate(from + 1, to) else ZChannel.unit ``` Now let's merge some channels: ```scala ZChannel .mergeAllUnbounded( ZChannel.writeAll( iterate(1, 3), iterate(4, 6), iterate(6, 9) ) ) .mapOutZIO(i => Console.print(i + " ")) .runDrain // Sample output: 1 4 6 7 8 2 3 5 6 9 ``` The `ZChannel.mergeAllUnbounded` uses the maximum buffer size, which is `Int.MaxValue` by default. This means that if we use this operator for long-running channels, which produce a lot of values, it can cause the program to run out of memory. We have another operator called `ZChannel.mergeAll`, which allows us to specify the buffer size, the concurrency level, and also the strategy for merging the channels. Note that if we want to merge channels sequentially, we can use the `zip` or `flatMap` operators: ```scala (iterate(1, 3) <*> iterate(4, 6) <*> iterate(6, 9)).runCollect.debug // Output: (Chunk(1,2,3,4,5,6,7,8,9),()) ``` ## Collecting 1. `collectElements` collects all the elements of the channel along with its done value as a tuple and returns a new channel with a terminal value of that tuple: ```scala ZChannel.writeAll(1,2,3,4,5) .collectElements .runCollect .debug // Output: (Chunk(),(Chunk(1,2,3,4,5),())) ``` 2. `emitCollect` is like the `collectElements` operator, but it emits the result of the collection to the output port of the new channel: ```scala ZChannel.writeAll(1,2,3,4,5) .emitCollect .runCollect .debug // Output: (Chunk((Chunk(1,2,3,4,5),())),()) ``` ## Converting We can convert a channel to other data types using the `ZChannel.toXYZ` methods: - `ZChannel#toStream` - `ZChannel#toPipeline` - `ZChannel#toSink` - `ZChannel#toPull` - `ZChannel#toQueue` ## concatMap `concatMap` is a combination of two operators: mapping and concatenation. Using this operator, we can map every emitted element of a channel (outer channel) to a new channel (inner channels), and then concatenate all the inner channels into a single channel. The concatenation is done **sequentially**, so we use this operator when the order of the elements is important: ```scala ZChannel .writeAll("a", "b", "c") .concatMap { l => def inner(from: Int, to: Int): ZChannel[Any, Any, Any, Any, Nothing, String, Unit] = if (from <= to) ZChannel.write(s"$l$from") *> inner(from + 1, to) else ZChannel.unit inner(0, 5) } .runCollect .debug // Output: (Chunk(a0,a1,a2,a3,a4,a5,b0,b1,b2,b3,b4,b5,c0,c1,c2,c3,c4,c5),()) ``` In the above example, we create a new channel for every element of the outer channel. The new inner channel is responsible for emitting from zero to five with the label of the outer channel. When an inner channel is done, it moves to the next inner channel sequentially. There is a similar operator called `mergeMap` that works in parallel and doesn't preserve the order of the elements. ## mergeMap `mergeMap` is a combination of two operators: mapping and merging. Using this operator, we can map every emitted element of a channel (outer channel) to a new channel (inner channel), and then run all the inner channels in parallel and merge them into a single channel. The merge operation is done **in parallel**, so we use this operator when the order of the elements is not important, and we want to process all inner channels in parallel: ```scala ZChannel .writeAll("a", "b", "c") .mergeMap(8, 1, MergeStrategy.BackPressure) { l => def inner( from: Int, to: Int ): ZChannel[Any, Any, Any, Any, Nothing, String, Unit] = if (from <= to) ZChannel.write(s"$l$from") *> inner(from + 1, to) else ZChannel.unit inner(0, 5) } .runCollect .debug // Non-deterministic output: (Chunk(a0,a1,a2,b0,b1,b2,b3,c0,b4,c1,a3,c2,b5,a4,c3,c4,a5,c5),()) ``` ## collect `collect` is a combination of two operations: filtering and mapping. Using this operator, we can filter the elements of a channel using a partial function, and then map the filtered elements: ```scala ZChannel .writeAll((1 to 10): _*) .collect { case i if i % 3 == 0 => i * 2 } .runCollect .debug // Output: (Chunk(6,12,18),()) ``` --- ## Composing Channels We can write more complex channels by using `read` operators and composing them recursively. Let's try some examples: ## Simple Echo Channel Assume we want to read a value from the input port and then print it to the console, we can use the `ZChannel.readWith` operator to do this: ```scala val producer = ZChannel.write(1) val consumer = ZChannel.readWith( (i: Int) => ZChannel.fromZIO(Console.printLine("Consumed: " + i)), (_: Any) => ZChannel.unit, (_: Any) => ZChannel.unit ) (producer >>> consumer).run // Output: // Consumed: 1 ``` ## Echo Channel Forever We can also recursively compose channels to create a more complex channel. In the following example, we are going to continuously read values from the console and write them back to the console: ```scala object MainApp extends ZIOAppDefault { val producer: ZChannel[Any, Any, Any, Any, IOException, String, Nothing] = ZChannel .fromZIO(Console.readLine("Please enter some text: ")) .flatMap(i => ZChannel.write(i) *> producer) val consumer: ZChannel[Any, Any, String, Any, IOException, Nothing, Unit] = ZChannel.readWith( (i: String) => i match { case "exit" => ZChannel.unit case _ => ZChannel.fromZIO(Console.printLine("Consumed: " + i)) *> consumer }, (_: Any) => ZChannel.unit, (_: Any) => ZChannel.unit ) def run = (producer >>> consumer).run } // Output: // Please enter some text: Foo // Consumed: Foo // Please enter some text: Bar // Consumed: Bar // Please enter some text: Baz // Consumed: Baz // Please enter some text: exit ``` ## Replicator Channel In this example, we are going to create a channel that replicates any input values to the output port. ```scala object MainApp extends ZIOAppDefault { lazy val doubler: ZChannel[Any, Any, Int, Any, Nothing, Int, Unit] = ZChannel.readWith( (i: Int) => ZChannel.writeAll(i, i) *> doubler, (_: Any) => ZChannel.unit, (_: Any) => ZChannel.unit ) def run = (ZChannel.writeAll(1,2,3,4,5) >>> doubler).runCollect.debug } // Output: // (Chunk(1,1,2,2,3,3,4,4,5,5),()) ``` ## Counter Channel We can also use `Ref` to create a channel with an updatable state. For example, we can create a channel that keeps track number of all the values that it has read and finally returns it as the done value: ```scala object MainApp extends ZIOAppDefault { val counter = { def count(c: Int): ZChannel[Any, Any, Int, Any, String, Int, Int] = ZChannel.readWith( (i: Int) => ZChannel.write(i) *> count(c + 1), (_: Any) => ZChannel.fail("error"), (_: Any) => ZChannel.succeed(c) ) count(0) } def run = (ZChannel.writeAll(1, 2, 3, 4, 5) >>> counter).runCollect.debug } // Output: // (Chunk(1,2,3,4,5), 5) ``` ## Dedupe Channel Sometimes we want to remove duplicate values from the input port. We need to have a state that keeps track of the values that have been seen. So if a value is seen for the first time, we can write it to the output port. If a value is duplicated, we can ignore it: ```scala object MainApp extends ZIOAppDefault { val dedup = ZChannel.fromZIO(Ref.make[HashSet[Int]](HashSet.empty)).flatMap { ref => lazy val inner: ZChannel[Any, Any, Int, Any, Nothing, Int, Unit] = ZChannel.readWith( (i: Int) => ZChannel .fromZIO(ref.modify(s => (s contains i, s incl i))) .flatMap { case true => ZChannel.unit case false => ZChannel.write(i) } *> inner, (_: Any) => ZChannel.unit, (_: Any) => ZChannel.unit ) inner } def run = (ZChannel.writeAll(1, 2, 2, 3, 3, 4, 2, 5, 5) >>> dedup).runCollect.debug } // Output: // (Chunk(1,2,3,4,5),()) ``` ## Buffered Channel With help of `ZChannel.buffer` or `ZChannel.bufferChunk`, we can create a channel backed by a buffer. - If the buffer is full, the channel puts the values in the buffer to the output port. - If the buffer is empty, the channel reads the value from the input port and puts it in the output port. Assume we have a channel written as follows: ```scala def buffered(input: Int) = ZChannel .fromZIO(Ref.make(input)) .flatMap { ref => ZChannel.buffer[Any, Int, Unit]( 0, i => if (i == 0) true else false, ref ) } ``` If the buffer is empty (zero value), the `buffered` channel passes the `1` to the output port: ```scala (ZChannel.write(1) >>> buffered(0)).runCollect.debug ``` If the buffer is full, the channel puts the value from the buffer to the output port: ```scala (ZChannel.write(1) >>> buffered(0)).runCollect.debug ``` --- ## Creating Channels `ZChannel` have several constructors and also built-in channels, where suitable to create more complex channels. Without further ado, let's learn them one by one: ## `ZChannel.succeed` Creates a channel that succeeds with a given done value, e.g. `ZChannel.succeed(42)`: ```scala val channel: ZChannel[Any, Any, Any, Any, Nothing, Nothing, Int] = ZChannel.succeed(42) ``` This channel doesn't produce any data but succeeds with a done value of type `Int`. Let's try to `runCollect` this channel and see what happens: ```scala channel.runCollect.debug ``` The output of the `runCollect` operation is a tuple of two elements: the first is a chunk of data that the channel produced, and the second is the done value. Because this channel doesn't produce any data, the first element is an empty chunk, but it has a 42 as the done value in the second element. ## `ZChannel.fail` Creates a channel that fails with a given error, e.g. `ZChannel.fail(new Exception("error"))`: ```scala val channel: ZChannel[Any, Any, Any, Any, Exception, Nothing, Nothing] = ZChannel.fail(new Exception("error")) ``` ## `ZChannel.write*` Create a channel that writes given elements to the output port: ```scala ZChannel.write(1).runCollect.debug // Output: (Chunk(1),()) ZChannel.writeAll(1, 2, 3).runCollect.debug // Output: (Chunk(1,2,3),()) ZChannel.writeChunk(Chunk(1, 2, 3)).runCollect.debug // Output: (Chunk(1,2,3),()) ``` ## `ZChannel.read*` Create a channel that reads elements from the input port and returns that as a done value: Let's start with the simplest read operation, `ZChannel.read`: ```scala val read: ZChannel[Any, Any, Int, Any, None.type, Nothing, Int] = ZChannel.read[Int] ``` To test this channel, we can create a writer channel and then pipe that to the reader channel: ```scala val read = ZChannel.read[Int] (ZChannel.write(1) >>> read).runCollect.debug // Output: (Chunk(0),1) ``` In the above example, the writer channel writes the value 1 to the output port, and the reader channel reads the value from the input port and then returns it as a done value. If we compose multiple read operations, we can read more values from the input port: ```scala val read = ZChannel.read[Int] (ZChannel.writeAll(1, 2, 3) >>> (read *> read)).runCollect.debug // Output: (Chunk(),2) (ZChannel.writeAll(1, 2, 3) >>> (read *> read *> read)).runCollect.debug // Output: (Chunk(),3) ``` Another useful read operation is `ZChannel.readWith`. Using this operator, after reading a value from the input port, instead of returning it as a done value, we have the ability to pass the input value to another channel. --- ## Introduction To ZChannels Channels are the nexus of communications, which support both reading and writing. They allow us to have a unidirectional flow of data from the input to the output. A `ZChannel[-Env, -InErr, -InElem, -InDone, +OutErr, +OutElem, +OutDone]` requires some environment `Env` and have two main operations: - It can read some data `InElem` from the input port, and finally can terminate with a done value of type `InDone`. If the read operation fails, the channel will terminate with an error of type `InErr`. - It can write some data `OutElem` to the output port, and finally terminate the channel with a done value of type `OutDone`. If the write operation fails, the channel will terminate with an error of type `OutErr`. They are an underlying abstraction for `ZStream`, `ZPipeline`, and `ZSink`. In ZIO Streams, we call the input port `ZStream`, the output port `ZSink`, and the middle part `ZPipeline`: - A `Channel` can write some elements to the _output_, and it can terminate with some sort of _done_ value. The `Channel` uses this _done_ value to notify the downstream `Channel` that its emission of elements is finished. In ZIO, the `ZStream` is encoded as an output side of the `Channel`. - A `Channel` can read from its input, and it can also terminate with some sort of _done_ value, which is an upstream result. So a `Channel` has the _input type_, and the _input done type_. The `Channel` uses this _done_ value to determine when the upstream `Channel` finishes its emission. In ZIO, the `ZSink` is encoded as an input side of the `Channel`. - A `Channel` can read from its input, do some transformation on the elements, and write to its output. In ZIO, the `ZPipeline` is encoded as a middle part of both sides of the `Channel`. Pipelines accept a stream as input and return the transformed stream as output. :::caution `ZChannel` is an underlying abstraction. So we do not usually need to use it directly. So if you are learning ZIO Streams, we recommend you to focus on `ZStream`, `ZPipeline`, and `ZSink` data types. ::: Let's take a look at how `ZStream`, `ZPipeline` and `ZSink` are defined using `ZChannel`: ```scala trait ZChannel[-Env, -InErr, -InElem, -InDone, +OutErr, +OutElem, +OutDone] case class ZStream[-R, +E, +A] ( val channel: ZChannel[R, Any, Any, Any, E, Chunk[A], Any] ) case class ZSink[-R, +E, -In, +L, +Z] ( val channel: ZChannel[R, ZNothing, Chunk[In], Any, E, Chunk[L], Z] ) case class ZPipeline[-R, +E, -In, +Out] ( val channel: ZChannel[R, ZNothing, Chunk[In], Any, E, Chunk[Out], Any] ) ``` So we can say that: - `ZStream[R, E, A]` is a channel that requires an environment `R`, emits elements of type `Chunk[A]`, and terminates, if at all, with either an error of type `E` or a done value of type `Any`. - `ZPipeline[R, Err, In, Out]` is a channel that uses `R` as its environment, consumes `Chunk[In]` from its input port, and produces `Chunk[Out]` to its output port. - `ZSink[R, E, In, L , Z]` is a channel that uses `R` as its environment, consumes `Chunk[In]` from its input port, and produces `Chunk[L]` to its output port as its leftovers, and can terminate with a success value of type `Z` or can terminate with a failure of type `E`. ![ZIO Streams 2.x](/img/assets/zio-streams-2.x.svg) Channels compose in a variety of ways: - **Piping**— One channel can be piped to another channel, assuming the input type of the second is the same as the output type of the first. We can pipe data from a channel that reads from the input port to a channel that writes to the output port, by using the `pipeTo` or `>>>` operator. - **Sequencing**— The terminal value of one channel can be used to create another channel, and both the first channel and the function that makes the second channel can be composed into a channel. We use the `ZChannel#flatMap` to sequence the channels. - **Concating**— The output of one channel can be used to create other channels, which are all concatenated together. The first channel and the function that makes the other channels can be composed into a channel. We use `ZChannel#concat*` operators to do this. Finally, we can run a channel by using the `ZChannel#run*` operators. --- ## Running a Channel To run a channel, we can use the `ZChannel.runXYZ` methods: - `ZChannel#run`— The `run` method is the simplest way to run a channel. It only runs a channel that doesn't read any input or write any output. - `ZChannel#runCollect`— It will run a channel and collects the output and finally returns it along with the done value of the channel. - `ZChannel#runDrain`— It will run a channel and ignore any emitted output. - `ZChannel#runScoped`— Using this method, we can run a channel in a scope. So all the finalizers of the scope will be run before the channel is closed. --- ## ZPipeline ## Introduction A `ZPipeline[-Env, +Err, -In, +Out]` is a stream transformer. Pipelines accept a stream as input and return the transformed stream as output. ZPipelines can be thought of as a recipe for calling a bunch of methods on a source stream to yield a new (transformed) stream. A nice mental model is the following type alias: ```scala type ZPipeline[Env, Err, In, Out] = ZStream[Env, Err, In] => ZStream[Env, Err, Out] ``` There is no fundamental requirement for pipelines to exist, because everything pipelines do can be done directly on a stream. However, because pipelines separate the stream transformation from the source stream itself, it becomes possible to abstract over stream transformations at the level of values, creating, storing, and passing around reusable transformation pipelines that can be applied to many different streams. ## Creation ### From Function By using `ZPipeline.map` we convert a function into a pipeline. Let's create a pipeline which converts a stream of strings into a stream of characters: ```scala val chars = ZPipeline.map[String, Chunk[Char]](s => Chunk.fromArray(s.toArray)) >>> ZPipeline.mapChunks[Chunk[Char], Char](_.flatten) ``` There is also a `ZPipeline.mapZIO` which is an effectful version of this constructor. ### From Custom Channels For stateful transformations that can't be expressed with `map` or `mapZIO`, you can build pipelines directly from `ZChannel` using `ZChannel.readWithCause`. Here is a pipeline that pairs each element with its predecessor: ```scala def pairwise[A]: ZPipeline[Any, Nothing, A, (A, A)] = ZPipeline.fromChannel(pairwiseGo[A](None)) def pairwiseGo[A]( prev: Option[A] ): ZChannel[Any, ZNothing, Chunk[A], Any, ZNothing, Chunk[(A, A)], Any] = ZChannel.readWithCause( (in: Chunk[A]) => { val buf = Chunk.newBuilder[(A, A)] var last = prev in.foreach { a => last.foreach(p => buf += ((p, a))) last = Some(a) } val out = buf.result() (if (out.nonEmpty) ZChannel.write(out) else ZChannel.unit) *> pairwiseGo(last) }, (err: Cause[ZNothing]) => ZChannel.refailCause(err), (_: Any) => ZChannel.unit ) ``` The three-case `readWithCause` pattern handles: - **Case 1 (in):** Process incoming chunk, emit outputs, recurse to read next chunk - **Case 2 (err):** Handle errors from upstream (usually propagate with `refailCause`) - **Case 3 (done):** Handle stream completion (finalize any pending state) :::note Use `ZNothing` (not `Nothing`) as the error type in channel signatures. `ZNothing` is ZIO's abstract bottom type that avoids Scala type inference issues when composing channels with `readWithCause`. ::: :::info **Why channels instead of functions?** Pipelines are channels because they need to compose seamlessly with sinks and other pipelines. A function-based pipeline couldn't be composed with a sink—the channel abstraction provides a unified interface for all streaming components. ::: ## Built-in Pipelines ### Identity The identity pipeline passes elements through without any modification: ```scala ZStream(1,2,3).via(ZPipeline.identity[Int]) // Ouput: 1, 2, 3 ``` ### Splitting **ZPipeline.splitOn** — A pipeline that splits strings on a delimiter: ```scala ZStream( "5-6-7-8", "-9-10-1", "1-12-13" ) .via(ZPipeline.splitOn("-")) .map(_.toInt) // Ouput: 5, 6, 7, 8, 9, 10, 11, 12, 13 ``` **ZPipeline.splitLines** — A pipeline that splits strings on newlines. Handles both Windows newlines (`\r\n`) and UNIX newlines (`\n`): ```scala ZStream("This is the first line.\nSecond line.\nAnd the last line.") .via(ZPipeline.splitLines) // Output: "This is the first line.", "Second line.", "And the last line." ``` **ZPipeline.splitOnChunk** — A pipeline that splits elements on a delimiter and transforms the splits into desired output: ```scala ZStream(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) .via(ZPipeline.splitOnChunk(Chunk(4, 5, 6))) // Output: Chunk(1, 2, 3), Chunk(7, 8, 9, 10) ``` ### Dropping **ZPipeline.dropWhile** — Creates a pipeline that starts consuming values as soon as one fails the given predicate: ```scala ZStream(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) .via(ZPipeline.dropWhile((x: Int) => x <= 5)) // Output: 6, 7, 8, 9, 10 ``` The `ZPipeline` also has `dropWhileZIO` which takes an effectful predicate `p: I => ZIO[R, E, Boolean]`. ### Prepending The `ZPipeline.prepend` creates a pipeline that emits the provided chunks before emitting any other values: ```scala ZStream(2, 3, 4).via( ZPipeline.prepend(Chunk(0, 1)) ) // Output: 0, 1, 2, 3, 4 ``` ### Compression **ZPipeline.deflate** — The `deflate` pipeline compresses a stream of bytes as specified by [RFC 1951](https://tools.ietf.org/html/rfc1951). ```scala def compressWithDeflate(clearText: ZStream[Any, Nothing, Byte]): ZStream[Any, Nothing, Byte] = { val bufferSize: Int = 64 * 1024 // Internal buffer size. Few times bigger than upstream chunks should work well. val noWrap: Boolean = false // For HTTP Content-Encoding should be false. val level: CompressionLevel = CompressionLevel.DefaultCompression val strategy: CompressionStrategy = CompressionStrategy.DefaultStrategy val flushMode: FlushMode = FlushMode.NoFlush clearText.via(deflate(bufferSize, noWrap, level, strategy, flushMode)) } def deflateWithDefaultParameters(clearText: ZStream[Any, Nothing, Byte]): ZStream[Any, Nothing, Byte] = clearText.via(deflate()) ``` **ZPipeline.gzip** — The `gzip` pipeline compresses a stream of bytes as using _gzip_ method: ```scala ZStream .fromFileName("file.txt") .via( ZPipeline.gzip( bufferSize = 64 * 1024, level = CompressionLevel.DefaultCompression, strategy = CompressionStrategy.DefaultStrategy, flushMode = FlushMode.NoFlush ) ) .run( ZSink.fromFileName("file.gz") ) ``` ### Decompression If we are reading `Content-Encoding: deflate`, `Content-Encoding: gzip` streams, or other such streams of compressed data, the following pipelines can be helpful. Both decompression methods will fail with `CompressionException` when input wasn't properly compressed: **ZPipeline.inflate** — This pipeline allows decompressing stream of _deflated_ inputs, according to [RFC 1951](https://tools.ietf.org/html/rfc1951). ```scala def decompressDeflated(deflated: ZStream[Any, Nothing, Byte]): ZStream[Any, CompressionException, Byte] = { val bufferSize: Int = 64 * 1024 // Internal buffer size. Few times bigger than upstream chunks should work well. val noWrap: Boolean = false // For HTTP Content-Encoding should be false. deflated.via(inflate(bufferSize, noWrap)) } ``` **ZPipeline.gunzip** — This pipeline can be used to decompress stream of _gzipped_ inputs, according to [RFC 1952](https://tools.ietf.org/html/rfc1952): ```scala def decompressGzipped(gzipped: ZStream[Any, Nothing, Byte]): ZStream[Any, CompressionException, Byte] = { val bufferSize: Int = 64 * 1024 // Internal buffer size. Few times bigger than upstream chunks should work well. gzipped.via(gunzip(bufferSize)) } ``` **ZPipeline.gunzipAuto** — This pipeline can be used to decompress stream of *possibly* _gzipped_ inputs, according to [RFC 1952](https://tools.ietf.org/html/rfc1952). If the input is gzipped, it will be decompressed; if not, it will be passed downstream as-is: ```scala def decompressMaybeGzipped(maybeGzipped: ZStream[Any, Nothing, Byte]): ZStream[Any, CompressionException, Byte] = { val bufferSize: Int = 64 * 1024 // Internal buffer size. Few times bigger than upstream chunks should work well. maybeGzipped.via(gunzipAuto(bufferSize)) } ``` ### Decoders ZIO stream has a wide variety of pipelines to decode chunks of bytes into strings: | Decoder | Input | Output | |-----------------------------|----------------|--------| | `ZPipeline.utfDecode` | Unicode bytes | String | | `ZPipeline.utf8Decode` | UTF-8 bytes | String | | `ZPipeline.utf16Decode` | UTF-16 | String | | `ZPipeline.utf16BEDecode` | UTF-16BE bytes | String | | `ZPipeline.utf16LEDecode` | UTF-16LE bytes | String | | `ZPipeline.utf32Decode` | UTF-32 bytes | String | | `ZPipeline.utf32BEDecode` | UTF-32BE bytes | String | | `ZPipeline.utf32LEDecode` | UTF-32LE bytes | String | | `ZPipeline.usASCIIDecode` | US-ASCII bytes | String | ## Operations ### Output Transformation (Mapping) To transform the _outputs_ of the pipeline, we can use the `ZPipeline#map` combinator for the success channel, and the `ZPipeline#mapError` combinator for the failure channel. Also, the `ZPipeline.mapChunks` takes a function of type `Chunk[O] => Chunk[O2]` and transforms chunks emitted by the pipeline. ### Input Transformation (Contramap) To transform the _inputs_ of the pipeline, we can use the `ZPipeline#contramap` combinator. It takes a map function of type `J => I` and convert a `ZPipeline[R, E, I, O]` to `ZPipeline[R, E, J, O]`: ```scala class ZPipeline[-R, +E, -I, +O] { final def contramap[J](f: J => I): ZPipeline[R, E, J, O] = ??? } ``` Let's create an integer parser pipeline using `ZPipeline.contramap`: ```scala val numbers: ZStream[Any, Nothing, Int] = ZStream("1-2-3-4-5") .mapConcat(_.split("-")) .via( ZPipeline.map[String, Int](_.toInt) ) ``` ### Composing We can compose pipelines in two ways: 1. **Composing Two Pipelines** — One pipeline can be composed with another pipeline, resulting in a composite pipeline: ```scala val lines: ZStream[Any, Throwable, String] = ZStream .fromFileName("file.txt") .via( ZPipeline.utf8Decode >>> ZPipeline.splitLines ) ``` 2. **Composing ZPipeline with ZSink** — One pipeline can be composed with a sink, resulting in a sink that processes elements by piping them through the pipeline and piping the results into the sink: ```scala val refine: ZIO[Any, Throwable, Long] = { val stream: ZStream[Any, Throwable, Byte] = ZStream.fromFileName("file.txt") val pipeline: ZPipeline[Any, CharacterCodingException, Byte, String] = ZPipeline.utf8Decode >>> ZPipeline.splitLines >>> ZPipeline.filter[String](_.contains('₿')) val fileSink: ZSink[Any, Throwable, String, Byte, Long] = ZSink .fromFileName("file.refined.txt") .contramapChunks[String]( _.flatMap(line => (line + System.lineSeparator()).getBytes()) ) val pipeSink: ZSink[Any, Throwable, Byte, Byte, Long] = pipeline >>> fileSink stream >>> pipeSink } ``` --- ## Parallel Operators ## Parallel Zipping Like `ZStream`, two `ZSink` can be zipped together. Both of them will be run in parallel, and their results will be combined in a tuple: ```scala val kafkaSink: ZSink[Any, Throwable, Record, Record, Unit] = ZSink.foreach[Any, Throwable, Record](record => ZIO.attempt(???)) val pulsarSink: ZSink[Any, Throwable, Record, Record, Unit] = ZSink.foreach[Any, Throwable, Record](record => ZIO.attempt(???)) val stream: ZSink[Any, Throwable, Record, Record, Unit] = kafkaSink zipPar pulsarSink ``` ## Racing We are able to `race` multiple sinks, they will run in parallel, and the one that wins will provide the result of our program: ```scala val stream: ZSink[Any, Throwable, Record, Record, Unit] = kafkaSink race pulsarSink ``` To determine which one succeeded, we should use the `ZSink#raceBoth` combinator, it returns an `Either` result. --- ## Creating Sinks The `zio.stream` provides numerous kinds of sinks to use. ### Common Constructors **ZSink.head** — It creates a sink containing the first element, returns `None` for empty streams: ```scala val sink: ZSink[Any, Nothing, Int, Int, Option[Int]] = ZSink.head[Int] val head: ZIO[Any, Nothing, Option[Int]] = ZStream(1, 2, 3, 4).run(sink) // Result: Some(1) ``` **ZSink.last** — It consumes all elements of a stream and returns the last element of the stream: ```scala val sink: ZSink[Any, Nothing, Int, Nothing, Option[Int]] = ZSink.last[Int] val last: ZIO[Any, Nothing, Option[Int]] = ZStream(1, 2, 3, 4).run(sink) // Result: Some(4) ``` **ZSink.count** — A sink that consumes all elements of the stream and counts the number of elements fed to it: ```scala val sink : ZSink[Any, Nothing, Int, Nothing, Long] = ZSink.count val count: ZIO[Any, Nothing, Long] = ZStream(1, 2, 3, 4, 5).run(sink) // Result: 5 ``` **ZSink.sum** — A sink that consumes all elements of the stream and sums incoming numeric values: ```scala val sink : ZSink[Any, Nothing, Int, Nothing, Int] = ZSink.sum[Int] val sum: ZIO[Any, Nothing, Int] = ZStream(1, 2, 3, 4, 5).run(sink) // Result: 15 ``` **ZSink.take** — A sink that takes the specified number of values and results in a `Chunk` data type: ```scala val sink : ZSink[Any, Nothing, Int, Int, Chunk[Int]] = ZSink.take[Int](3) val stream: ZIO[Any, Nothing, Chunk[Int]] = ZStream(1, 2, 3, 4, 5).run(sink) // Result: Chunk(1, 2, 3) ``` **ZSink.drain** — A sink that ignores its inputs: ```scala val drain: ZSink[Any, Nothing, Any, Nothing, Unit] = ZSink.drain ``` **ZSink.timed** — A sink that executes the stream and times its execution: ```scala val timed: ZSink[Any, Nothing, Any, Nothing, Duration] = ZSink.timed val stream: ZIO[Any, Nothing, Long] = ZStream(1, 2, 3, 4, 5) .schedule(Schedule.fixed(2.seconds)) .run(timed) .map(_.getSeconds) // Result: 10 ``` **ZSink.foreach** — A sink that executes the provided effectful function for every element fed to it: ```scala val printer: ZSink[Any, IOException, Int, Int, Unit] = ZSink.foreach((i: Int) => printLine(i)) val stream : ZIO[Any, IOException, Unit] = ZStream(1, 2, 3, 4, 5).run(printer) ``` ### From Success and Failure Similar to the `ZStream` data type, we can create a `ZSink` using `fail` and `succeed` methods. A sink that doesn't consume any element from its upstream and succeeds with a value of `Int` type: ```scala val succeed: ZSink[Any, Any, Any, Nothing, Int] = ZSink.succeed(5) ``` A sink that doesn't consume any element from its upstream and intentionally fails with a message of `String` type: ```scala val failed : ZSink[Any, String, Any, Nothing, Nothing] = ZSink.fail("fail!") ``` ### Collecting To create a sink that collects all elements of a stream into a `Chunk[A]`, we can use `ZSink.collectAll`: ```scala val stream : UStream[Int] = ZStream(1, 2, 3, 4, 5) val collection: UIO[Chunk[Int]] = stream.run(ZSink.collectAll[Int]) // Output: Chunk(1, 2, 3, 4, 5) ``` We can collect all elements into a `Set`: ```scala val collectAllToSet: ZSink[Any, Nothing, Int, Nothing, Set[Int]] = ZSink.collectAllToSet[Int] val stream: ZIO[Any, Nothing, Set[Int]] = ZStream(1, 3, 2, 3, 1, 5, 1).run(collectAllToSet) // Output: Set(1, 3, 2, 5) ``` Or we can collect and merge them into a `Map[K, A]` using a merge function. In the following example, we use `(_:Int) % 3` to determine map keys, and we provide the `_ + _` function to merge multiple elements with the same key: ```scala val collectAllToMap: ZSink[Any, Nothing, Int, Nothing, Map[Int, Int]] = ZSink.collectAllToMap((_: Int) % 3)(_ + _) val stream: ZIO[Any, Nothing, Map[Int, Int]] = ZStream(1, 3, 2, 3, 1, 5, 1).run(collectAllToMap) // Output: Map(1 -> 3, 0 -> 6, 2 -> 7) ``` **ZSink.collectAllN** — Collects incoming values into a chunk of maximum size of `n`: ```scala ZStream(1, 2, 3, 4, 5).run( ZSink.collectAllN(3) ) // Output: Chunk(1,2,3), Chunk(4,5) ``` **ZSink.collectAllWhile** — Accumulates incoming elements into a chunk as long as they verify the given predicate: ```scala ZStream(1, 2, 0, 4, 0, 6, 7).run( ZSink.collectAllWhile(_ != 0) ) // Output: Chunk(1,2), Chunk(4), Chunk(6,7) ``` **ZSink.collectAllToMapN** — Creates a sink accumulating incoming values into maps of up to `n` keys. Elements are mapped to keys using the function `key`; elements mapped to the same key will be merged with the function `f`: ```scala object ZSink { def collectAllToMapN[Err, In, K]( n: Long )(key: In => K)(f: (In, In) => In): ZSink[Any, Err, In, Err, In, Map[K, In]] } ``` Let's do an example: ```scala ZStream(1, 2, 0, 4, 5).run( ZSink.collectAllToMapN[Nothing, Int, Int](10)(_ % 3)(_ + _) ) // Output: Map(1 -> 5, 2 -> 7, 0 -> 0) ``` **ZSink.collectAllToSetN** — Creates a sink accumulating incoming values into sets of maximum size `n`: ```scala ZStream(1, 2, 1, 2, 1, 3, 0, 5, 0, 2).run( ZSink.collectAllToSetN(3) ) // Output: Set(1,2,3), Set(0,5,2), Set(1) ``` ### Folding Basic fold accumulation of received elements: ```scala ZSink.foldLeft[Int, Int](0)(_ + _) ``` A fold with short-circuiting has a termination predicate that determines the end of the folding process: ```scala ZStream.iterate(0)(_ + 1).run( ZSink.fold(0)(sum => sum <= 10)((acc, n: Int) => acc + n) ) // Output: 15 ``` **ZSink.foldWeighted** — Creates a sink that folds incoming elements until it reaches the `max` worth of elements determined by the `costFn`, then the pipeline emits the computed value and restarts the folding process: ```scala object ZSink { def foldWeighted[In, S](z: => S)(costFn: (S, In) => Long, max: Long)( f: (S, In) => S ): ZSink[Any, Nothing, In, In, S] = ??? } ``` In the following example, each time we consume a new element, we return one as the weight of that element using the cost function. After three times, the sum of the weights reaches the `max` number, and the folding process restarts. So we expect this pipeline to group each three elements in one `Chunk`: ```scala ZStream(3, 2, 4, 1, 5, 6, 2, 1, 3, 5, 6) .transduce( ZSink .foldWeighted(Chunk[Int]())( (_, _: Int) => 1, 3 ) { (acc, el) => acc ++ Chunk(el) } ) // Output: Chunk(3,2,4),Chunk(1,5,6),Chunk(2,1,3),Chunk(5,6) ``` Another example is when we want to group elements whose sum is equal to or less than a specific number: ```scala ZStream(1, 2, 2, 4, 2, 1, 1, 1, 0, 2, 1, 2) .transduce( ZSink .foldWeighted(Chunk[Int]())( (_, i: Int) => i.toLong, 5 ) { (acc, el) => acc ++ Chunk(el) } ) // Output: Chunk(1,2,2),Chunk(4),Chunk(2,1,1,1,0),Chunk(2,1,2) ``` :::caution The `ZSink.foldWeighted` cannot decompose elements whose weight is more than the `max` number. So elements that have an individual cost larger than `max` will force the pipeline to cross the `max` cost. In the last example, if the source stream was `ZStream(1, 2, 2, 4, 2, 1, 6, 1, 0, 2, 1, 2)` the output would be `Chunk(1,2,2),Chunk(4),Chunk(2,1),Chunk(6),Chunk(1,0,2,1),Chunk(2)`. As we see, the `6` element crossed the `max` cost. To decompose these elements, we should use the `ZSink.foldWeightedDecompose` function. ::: **ZSink.foldWeightedDecompose** — As we saw in the previous section, we need a way to decompose elements — whose cause the output aggregate cross the `max` — into smaller elements. This version of fold takes the `decompose` function and enables us to do that: ```scala object ZSink { def foldWeightedDecompose[Err, In, S]( z: S )(costFn: (S, In) => Long, max: Long, decompose: In => Chunk[In])( f: (S, In) => S ): ZSink[Any, Err, In, Err, In, S] = ??? } ``` In the following example, we are break down elements that are bigger than 5, using the `decompose` function: ```scala ZStream(1, 2, 2, 2, 1, 6, 1, 7, 2, 1, 2) .transduce( ZSink .foldWeightedDecompose(Chunk[Int]())( (_, i: Int) => i.toLong, 5, (i: Int) => if (i > 5) Chunk(i - 1, 1) else Chunk(i) )((acc, el) => acc ++ Chunk.succeed(el)) ) // Ouput: Chunk(1,2,2),Chunk(2,1),Chunk(5),Chunk(1,1),Chunk(5),Chunk(1,1,2,1),Chunk(2) ``` **ZSink.foldUntil** — Creates a sink that folds incoming elements until specific `max` elements have been folded: ```scala ZStream(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) .run(ZSink.foldUntil(0, 3)(_ + _)) // Output: 6, 15, 24, 10 ``` **ZSink.foldLeft** — This sink will fold the inputs until the stream ends, resulting in one element: ```scala val stream: ZIO[Any, Nothing, Int] = ZStream(1, 2, 3, 4).run(ZSink.foldLeft[Int, Int](0)(_ + _)) // Output: 10 ``` ### From ZIO The `ZSink.fromZIO` creates a single-value sink produced from a ZIO workflow: ```scala val sink = ZSink.fromZIO(ZIO.succeed(1)) ``` ### From File The `ZSink.fromPath` creates a file sink that consumes byte chunks and writes them to the specified file: ```scala def fileSink(path: Path): ZSink[Any, Throwable, String, Byte, Long] = ZSink .fromPath(path) .contramapChunks[String](_.flatMap(_.getBytes)) val result = ZStream("Hello", "ZIO", "World!") .intersperse("\n") .run(fileSink(Paths.get("file.txt"))) ``` ### From OutputStream The `ZSink.fromOutputStream` creates a sink that consumes byte chunks and writes them to the `OutputStream`: ```scala ZStream("Application", "Error", "Logs") .intersperse("\n") .run( ZSink .fromOutputStream(java.lang.System.err) .contramapChunks[String](_.flatMap(_.getBytes)) ) ``` ### From Queue A queue has a finite or infinite buffer size, so they are useful in situations where we need to consume streams as fast as we can and then do some batching operations on consumed messages. By using `ZSink.fromQueue`, we can create a sink that is backed by a queue; it enqueues each element into the specified queue: ```scala val myApp: IO[IOException, Unit] = for { queue <- Queue.bounded[Int](32) producer <- ZStream .iterate(1)(_ + 1) .schedule(Schedule.fixed(200.millis)) .run(ZSink.fromQueue(queue)) .fork consumer <- queue.take.flatMap(printLine(_)).forever _ <- producer.zip(consumer).join } yield () ``` ### From Hub `Hub` is an asynchronous data type in which a publisher can publish their messages to that and subscribers can subscribe to take messages from the `Hub`. The `ZSink.fromHub` takes a `Hub` and returns a `ZSink` which publishes each element to that `Hub`. In the following example, the `sink` consumes elements of the `producer` stream and publishes them to the `hub`. We have two consumers that are subscribed to that hub, and they are taking its elements forever: ```scala val myApp: ZIO[Any, IOException, Unit] = for { promise <- Promise.make[Nothing, Unit] hub <- Hub.bounded[Int](1) sink <- ZIO.succeed(ZSink.fromHub(hub)) producer <- ZStream .iterate(0)(_ + 1) .schedule(Schedule.fixed(1.seconds)) .run(sink) .fork consumers <- ZIO.scoped { hub.subscribe.zip(hub.subscribe).flatMap { case (left, right) => for { _ <- promise.succeed(()) f1 <- left.take.flatMap(e => printLine(s"Left Queue: $e")).forever.fork f2 <- right.take.flatMap(e => printLine(s"Right Queue: $e")).forever.fork _ <- f1.zip(f2).join } yield () } }.fork _ <- promise.await _ <- producer.zip(consumers).join } yield () ``` --- ## Introduction to ZSink A `ZSink[R, E, I, L, Z]` is used to consume elements produced by a `ZStream`. You can think of a sink as a function that will consume a variable amount of `I` elements (could be 0, 1, or many!), might fail with an error of type `E`, and will eventually yield a value of type `Z` together with a remainder of type `L` as leftover. To consume a stream using `ZSink` we can pass `ZSink` to the `ZStream#run` function: ```scala val stream = ZStream.fromIterable(1 to 1000) val sink = ZSink.sum[Int] val sum = stream.run(sink) ``` `ZSink` has one type alias called `Sink`. `Sink[E, A, L, B]` is a type alias for `ZSink[Any, E, A, L, B]`. We can think of a `Sink` as a function that does not require any services and will consume a variable amount of `A` elements (could be 0, 1, or many!), might fail with an error of type `E`, and will eventually yield a value of type `B`. The `L` is the type of elements in the leftover. ```scala type Sink[+E, A, +L, +B] = ZSink[Any, E, A, L, B] ``` --- ## Leftovers ## Collecting Leftovers A sink consumes a variable amount of `I` elements (zero or more) from the upstream. If the upstream is finite, we can collect leftover values by calling `ZSink#collectLeftover`. It returns a tuple that contains the result of the previous sink and its leftovers: ```scala val s1: ZIO[Any, Nothing, (Chunk[Int], Chunk[Int])] = ZStream(1, 2, 3, 4, 5).run( ZSink.take(3).collectLeftover ) // Output: (Chunk(1, 2, 3), Chunk(4, 5)) val s2: ZIO[Any, Nothing, (Option[Int], Chunk[Int])] = ZStream(1, 2, 3, 4, 5).run( ZSink.head[Int].collectLeftover ) // Output: (Some(1), Chunk(2, 3, 4, 5)) ``` ## Ignoring Leftovers If we don't need leftovers, we can drop them by using `ZSink#ignoreLeftover`: ```scala ZSink.take[Int](3).ignoreLeftover ``` --- ## Sink Operations Having created the sink, we can transform it with provided operations. ## contramap Contramap is a simple combinator to change the domain of an existing function. While _map_ changes the co-domain of a function, the _contramap_ changes the domain of a function. So the _contramap_ takes a function and maps over its input. This is useful when we have a fixed output, and our existing function cannot consume those outputs. So we can use _contramap_ to create a new function that can consume that fixed output. Assume we have a `ZSink.sum` that sums incoming numeric values, but we have a `ZStream` of `String` values. We can convert the `ZSink.sum` to a sink that can consume `String` values; ```scala val numericSum: ZSink[Any, Nothing, Int, Nothing, Int] = ZSink.sum[Int] val stringSum : ZSink[Any, Nothing, String, Nothing, Int] = numericSum.contramap((x: String) => x.toInt) val sum: ZIO[Any, Nothing, Int] = ZStream("1", "2", "3", "4", "5").run(stringSum) // Output: 15 ``` ## dimap A `dimap` is an extended `contramap` that additionally transforms sink's output: ```scala // Convert its input to integers, do the computation and then convert them back to a string val sumSink: ZSink[Any, Nothing, String, Nothing, String] = numericSum.dimap[String, String](_.toInt, _.toString) val sum: ZIO[Any, Nothing, String] = ZStream("1", "2", "3", "4", "5").run(sumSink) // Output: 15 ``` ## Filtering Sinks have `ZSink#filterInput` for filtering incoming elements: ```scala ZStream(1, -2, 0, 1, 3, -3, 4, 2, 0, 1, -3, 1, 1, 6) .transduce( ZSink .collectAllN[Int](3) .filterInput[Int](_ > 0) ) // Output: Chunk(Chunk(1,1,3),Chunk(4,2,1),Chunk(1,1,6),Chunk()) ``` --- ## Consuming Streams ```scala val result: Task[Unit] = ZStream.fromIterable(0 to 100).foreach(printLine(_)) ``` ### Using a Sink To consume a stream using `ZSink` we can pass `ZSink` to the `ZStream#run` function: ```scala val sum: UIO[Int] = ZStream(1,2,3).run(ZSink.sum) ``` ### Using fold The `ZStream#fold` method executes the fold operation over the stream of values and returns a `ZIO` effect containing the result: ```scala val s1: ZIO[Any, Nothing, Int] = ZStream(1, 2, 3, 4, 5).runFold(0)(_ + _) val s2: ZIO[Any, Nothing, Int] = ZStream.iterate(1)(_ + 1).runFoldWhile(0)(_ <= 5)(_ + _) ``` ### Using foreach Using `ZStream#foreach` is another way of consuming elements of a stream. It takes a callback of type `O => ZIO[R1, E1, Any]` which passes each element of a stream to this callback: ```scala ZStream(1, 2, 3).foreach(printLine(_)) ``` --- ## Creating ZIO Streams There are several ways to create ZIO Stream. In this section, we are going to enumerate some of the important ways of creating `ZStream`. ## Common Constructors **ZStream.apply** — Creates a pure stream from a variable list of values: ```scala val stream: ZStream[Any, Nothing, Int] = ZStream(1, 2, 3) ``` **ZStream.unit** — A stream that contains a single `Unit` value: ```scala val unit: ZStream[Any, Nothing, Unit] = ZStream.unit ``` **ZStream.never** — A stream that produces no value or fails with an error: ```scala val never: ZStream[Any, Nothing, Nothing] = ZStream.never ``` **ZStream.repeat** — A stream that repeats using the specified schedule: ```scala val repeat: ZStream[Any, Nothing, Int] = ZStream(1).repeat(Schedule.forever) ``` **ZStream.iterate** — Takes an initial value and applies the given function to the initial value iteratively. The initial value is the first value produced by the stream, followed by f(init), f(f(init)), ... ```scala val nats: ZStream[Any, Nothing, Int] = ZStream.iterate(1)(_ + 1) // 1, 2, 3, ... ``` **ZStream.range** — A stream from a range of integers `[min, max)`: ```scala val range: ZStream[Any, Nothing, Int] = ZStream.range(1, 5) // 1, 2, 3, 4 ``` **ZStream.service[R]** — Create a stream that extract the requested service from the environment: ```scala trait Foo val fooStream: ZStream[Foo, Nothing, Foo] = ZStream.service[Foo] ``` **ZStream.scoped** — Creates a single-valued stream from a scoped resource: ```scala val scopedStream: ZStream[Any, Throwable, BufferedReader] = ZStream.scoped( ZIO.fromAutoCloseable( ZIO.attemptBlocking( Files.newBufferedReader(java.nio.file.Paths.get("file.txt")) ) ) ) ``` ## From Success and Failure Similar to `ZIO` data type, we can create a `ZStream` using `fail` and `succeed` methods: ```scala val s1: ZStream[Any, String, Nothing] = ZStream.fail("Uh oh!") val s2: ZStream[Any, Nothing, Int] = ZStream.succeed(5) ``` ## From Chunks We can create a stream from a `Chunk`: ```scala val s1 = ZStream.fromChunk(Chunk(1, 2, 3)) // s1: ZStream[Any, Nothing, Int] = zio.stream.ZStream@115f3db6 ``` Or from multiple `Chunks`: ```scala val s2 = ZStream.fromChunks(Chunk(1, 2, 3), Chunk(4, 5, 6)) // s2: ZStream[Any, Nothing, Int] = zio.stream.ZStream@40b2f38e ``` ## From ZIO **ZStream.fromZIO** — We can create a stream from a ZIO workflow by using `ZStream.fromZIO` constructor. For example, the following stream is a stream that reads a line from a user: ```scala val readline: ZStream[Any, IOException, String] = ZStream.fromZIO(Console.readLine) ``` A stream that produces one random number: ```scala val randomInt: ZStream[Any, Nothing, Int] = ZStream.fromZIO(Random.nextInt) ``` **ZStream.fromZIOOption** — In some cases, depending on the result of the ZIO workflow, we should decide to emit an element or return an empty stream. In these cases, we can use `fromZIOOption` constructor: ```scala object ZStream { def fromZIOOption[R, E, A](fa: ZIO[R, Option[E], A]): ZStream[R, E, A] = ??? } ``` Let's see an example of using this constructor. In this example, we read a string from user input, and then decide to emit that or not; If the user enters an `EOF` string, we emit an empty stream, otherwise we emit the user input: ```scala val userInput: ZStream[Any, IOException, String] = ZStream.fromZIOOption( Console.readLine.mapError(Option(_)).flatMap { case "EOF" => ZIO.fail[Option[IOException]](None) case o => ZIO.succeed(o) } ) ``` ## From Asynchronous Callback Assume we have an asynchronous function that is based on callbacks. We would like to register a callbacks on that function and get back a stream of the results emitted by those callbacks. We have `ZStream.async` which can adapt functions that call their callbacks multiple times and emit the results over a stream: ```scala // Asynchronous Callback-based API def registerCallback( name: String, onEvent: Int => Unit, onError: Throwable => Unit ): Unit = ??? // Lifting an Asynchronous API to ZStream val stream = ZStream.async[Any, Throwable, Int] { cb => registerCallback( "foo", event => cb(ZIO.succeed(Chunk(event))), error => cb(ZIO.fail(error).mapError(Some(_))) ) } ``` The error type of the `register` function is optional, so by setting the error to the `None` we can use it to signal the end of the stream. ## From Iterators Iterators are data structures that allow us to iterate over a sequence of elements. Similarly, we can think of ZIO Streams as effectual Iterators; every `ZStream` represents a collection of one or more, but effectful values. **ZStream.fromIteratorSucceed** — We can convert an iterator that does not throw exception to `ZStream` by using `ZStream.fromIteratorSucceed`: ```scala val s1: ZStream[Any, Throwable, Int] = ZStream.fromIterator(Iterator(1, 2, 3)) val s2: ZStream[Any, Throwable, Int] = ZStream.fromIterator(Iterator.range(1, 4)) val s3: ZStream[Any, Throwable, Int] = ZStream.fromIterator(Iterator.continually(0)) ``` Also, there is another constructor called **`ZStream.fromIterator`** that creates a stream from an iterator which may throw an exception. **ZStream.fromIteratorZIO** — If we have an effectful Iterator that may throw Exception, we can use `fromIteratorZIO` to convert that to the ZIO Stream: ```scala val lines: ZStream[Any, Throwable, String] = ZStream.fromIteratorZIO(ZIO.attempt(Source.fromFile("file.txt").getLines())) ``` Using this method is not good for resourceful effects like above, so it's better to rewrite that using `ZStream.fromIteratorScoped` function. **ZStream.fromIteratorScoped** — Using this constructor we can convert a scoped iterator to ZIO Stream: ```scala val lines: ZStream[Any, Throwable, String] = ZStream.fromIteratorScoped( ZIO.fromAutoCloseable( ZIO.attempt(scala.io.Source.fromFile("file.txt")) ).map(_.getLines()) ) ``` **ZStream.fromJavaIterator** — It is the Java version of these constructors which create a stream from Java iterator that may throw an exception. We can convert any Java collection to an iterator and then lift them to the ZIO Stream. For example, to convert the Java Stream to the ZIO Stream, `ZStream` has a `fromJavaStream` constructor which convert the Java Stream to the Java Iterator and then convert that to the ZIO Stream using `ZStream.fromJavaIterator` constructor: ```scala def fromJavaStream[A](stream: => java.util.stream.Stream[A]): ZStream[Any, Throwable, A] = ZStream.fromJavaIterator(stream.iterator()) ``` Similarly, `ZStream` has `ZStream.fromJavaIteratorSucceed`, `ZStream.fromJavaIteratorZIO` and `ZStream.fromJavaIteratorScoped` constructors. ## From Iterables **ZStream.fromIterable** — We can create a stream from `Iterable` collection of values: ```scala val list = ZStream.fromIterable(List(1, 2, 3)) ``` **ZStream.fromIterableZIO** — If we have an effect producing a value of type `Iterable` we can use `fromIterableZIO` constructor to create a stream of that effect. Assume we have a database that returns a list of users using `Task`: ```scala trait Database { def getUsers: Task[List[User]] } object Database { def getUsers: ZIO[Database, Throwable, List[User]] = ZIO.serviceWithZIO[Database](_.getUsers) } ``` As this operation is effectful, we can use `ZStream.fromIterableZIO` to convert the result to the `ZStream`: ```scala val users: ZStream[Database, Throwable, User] = ZStream.fromIterableZIO(Database.getUsers) ``` ## From Repetition **ZStream.repeat** — Repeats the provided value infinitely: ```scala val repeatZero: ZStream[Any, Nothing, Int] = ZStream.repeat(0) ``` **ZStream.repeatWith** — This is another variant of `repeat`, which repeats according to the provided schedule. For example, the following stream produce zero value every second: ```scala val repeatZeroEverySecond: ZStream[Any, Nothing, Int] = ZStream.repeatWithSchedule(0, Schedule.spaced(1.seconds)) ``` **ZStream.repeatZIO** — Assume we have an effectful API, and we need to call that API and create a stream from the result of that. We can create a stream from that effect that repeats forever. Let's see an example of creating a stream of random numbers: ```scala val randomInts: ZStream[Any, Nothing, Int] = ZStream.repeatZIO(Random.nextInt) ``` **ZStream.repeatZIOOption** — We can repeatedly evaluate the given effect and terminate the stream based on some conditions. Let's create a stream repeatedly from user inputs until user enter "EOF" string: ```scala val userInputs: ZStream[Any, IOException, String] = ZStream.repeatZIOOption( Console.readLine.mapError(Option(_)).flatMap { case "EOF" => ZIO.fail[Option[IOException]](None) case o => ZIO.succeed(o) } ) ``` Here is another interesting example of using `repeatZIOOption`; In this example, we are draining an `Iterator` to create a stream of that iterator: ```scala def drainIterator[A](it: Iterator[A]): ZStream[Any, Throwable, A] = ZStream.repeatZIOOption { ZIO.attempt(it.hasNext).mapError(Some(_)).flatMap { hasNext => if (hasNext) ZIO.attempt(it.next()).mapError(Some(_)) else ZIO.fail(None) } } ``` **ZStream.tick** — A stream that emits Unit values spaced by the specified duration: ```scala val stream: ZStream[Any, Nothing, Unit] = ZStream.tick(1.seconds) ``` There are some other variant of repetition API like `repeatZIOWith`, `repeatZIOOption`, `repeatZIOChunk` and `repeatZIOChunkOption`. ## From Unfolding/Pagination In functional programming, `unfold` is dual to `fold`. With `fold` we can process a data structure and build a return value. For example, we can process a `List[Int]` and return the sum of all its elements. The `unfold` represents an operation that takes an initial value and generates a recursive data structure, one-piece element at a time by using a given state function. For example, we can create a natural number by using `one` as the initial element and the `inc` function as the state function. ### Unfold **ZStream.unfold** — `ZStream` has `unfold` function, which is defined as follows: ```scala object ZStream { def unfold[S, A](s: S)(f: S => Option[(A, S)]): ZStream[Any, Nothing, A] = ??? } ``` - **s** — An initial state value - **f** — A state function `f` that will be applied to the initial state `s`. If the result of this application is `None` the stream will end, otherwise the result is `Some`, so the next element in the stream would be `A` and the current state of transformation changed to the new `S`, this new state is the basis of the next unfold process. For example, we can a stream of natural numbers using `ZStream.unfold`: ```scala val nats: ZStream[Any, Nothing, Int] = ZStream.unfold(1)(n => Some((n, n + 1))) ``` We can write `countdown` function using `unfold`: ```scala def countdown(n: Int) = ZStream.unfold(n) { case 0 => None case s => Some((s, s - 1)) } ``` Running this function with an input value of 3 returns a `ZStream` which contains 3, 2, 1 values. **ZStream.unfoldZIO** — `unfoldZIO` is an effectful version of `unfold`. It helps us to perform _effectful state transformation_ when doing unfold operation. Let's write a stream of lines of input from a user until the user enters the `exit` command: ```scala val inputs: ZStream[Any, IOException, String] = ZStream.unfoldZIO(()) { _ => Console.readLine.map { case "exit" => None case i => Some((i, ())) } } ``` `ZStream.unfoldChunk`, and `ZStream.unfoldChunkZIO` are other variants of `unfold` operations but for `Chunk` data type. ### Pagination **ZStream.paginate** — This is similar to `unfold`, but allows the emission of values to end one step further. For example the following stream emits `0, 1, 2, 3` elements: ```scala val stream = ZStream.paginate(0) { s => s -> (if (s < 3) Some(s + 1) else None) } ``` Similar to `unfold` API, `ZStream` has various other forms as well as `ZStream.paginateZIO`, `ZStream.paginateChunk` and `ZStream.paginateChunkZIO`. ### Unfolding vs. Pagination One might ask what is the difference between `unfold` and `paginate` combinators? When we should prefer one over another? So, let's find the answer to this question by doing another example. Assume we have a paginated API that returns an enormous amount of data in a paginated fashion. When we call that API, it returns a data type `ResultPage` which contains the first-page result and, it also contains a flag indicating whether that result is the last one, or we have more data on the next page: ```scala case class PageResult(results: Chunk[RowData], isLast: Boolean) def listPaginated(pageNumber: Int): ZIO[Any, Throwable, PageResult] = ZIO.fail(???) ``` We want to convert this API to a stream of `RowData` events. For the first attempt, we might think we can do it by using `unfold` operation as below: ```scala val firstAttempt: ZStream[Any, Throwable, RowData] = ZStream.unfoldChunkZIO(0) { pageNumber => for { page <- listPaginated(pageNumber) } yield if (page.isLast) None else Some((page.results, pageNumber + 1)) } ``` But it doesn't work properly; it doesn't include the last page result. So let's do a trick and to perform another API call to include the last page results: ```scala val secondAttempt: ZStream[Any, Throwable, RowData] = ZStream.unfoldChunkZIO(Option[Int](0)) { case None => ZIO.none // We already hit the last page case Some(pageNumber) => // We did not hit the last page yet for { page <- listPaginated(pageNumber) } yield Some(page.results, if (page.isLast) None else Some(pageNumber + 1)) } ``` This works and contains all the results of returned pages. It works but as we saw, `unfold` is not friendliness to retrieve data from paginated APIs. We need to do some hacks and extra works to include results from the last page. This is where `ZStream.paginate` operation comes to play, it helps us to convert a paginated API to ZIO stream in a more ergonomic way. Let's rewrite this solution by using `paginate`: ```scala val finalAttempt: ZStream[Any, Throwable, RowData] = ZStream.paginateChunkZIO(0) { pageNumber => for { page <- listPaginated(pageNumber) } yield page.results -> (if (!page.isLast) Some(pageNumber + 1) else None) } ``` ## From Wrapped Streams Sometimes we have an effect that contains a `ZStream`, we can unwrap the embedded stream and produce a stream from those effects. If the stream is wrapped with the `ZIO` effect, we use `unwrap`, and if it is wrapped with scoped `ZIO` we use `unwrapScoped`: ```scala val wrappedWithZIO: UIO[ZStream[Any, Nothing, Int]] = ZIO.succeed(ZStream(1, 2, 3)) val s1: ZStream[Any, Nothing, Int] = ZStream.unwrap(wrappedWithZIO) val wrappedWithZIOScoped = ZIO.succeed(ZStream(1, 2, 3)) val s2: ZStream[Any, Nothing, Int] = ZStream.unwrapScoped(wrappedWithZIOScoped) ``` ## From Java IO **ZStream.fromPath** — Create ZIO Stream from a file: ```scala val file: ZStream[Any, Throwable, Byte] = ZStream.fromPath(Paths.get("file.txt")) ``` **ZStream.fromInputStream** — Creates a stream from a `java.io.InputStream`: ```scala val stream: ZStream[Any, IOException, Byte] = ZStream.fromInputStream(new FileInputStream("file.txt")) ``` Note that the InputStream will not be explicitly closed after it is exhausted. Use `ZStream.fromInputStreamZIO`, or `ZStream.fromInputStreamScoped` instead. **ZStream.fromInputStreamZIO** — Creates a stream from a `java.io.InputStream`. Ensures that the InputStream is closed after it is exhausted: ```scala val stream: ZStream[Any, IOException, Byte] = ZStream.fromInputStreamZIO( ZIO.attempt(new FileInputStream("file.txt")) .refineToOrDie[IOException] ) ``` **ZStream.fromInputStreamScoped** — Creates a stream from a scoped `java.io.InputStream` value: ```scala val scoped: ZIO[Scope, IOException, FileInputStream] = ZIO.fromAutoCloseable( ZIO.attempt(new FileInputStream("file.txt")) ).refineToOrDie[IOException] val stream: ZStream[Any, IOException, Byte] = ZStream.fromInputStreamScoped(scoped) ``` **ZStream.fromResource** — Create a stream from resource file: ```scala val stream: ZStream[Any, IOException, Byte] = ZStream.fromResource("file.txt") ``` **ZStream.fromReader** — Creates a stream from a `java.io.Reader`: ```scala val stream: ZStream[Any, IOException, Char] = ZStream.fromReader(new FileReader("file.txt")) ``` ZIO Stream also has `ZStream.fromReaderZIO` and `ZStream.fromReaderScoped` variants. ## From Java Stream We can use `ZStream.fromJavaStreamTotal` to convert a Java Stream to ZIO Stream: ```scala val stream: ZStream[Any, Throwable, Int] = ZStream.fromJavaStream(java.util.stream.Stream.of(1, 2, 3)) ``` ZIO Stream also has `ZStream.fromJavaStream`, `ZStream.fromJavaStreamZIO` and `ZStream.fromJavaStreamScoped` variants. ## From Queue and Hub `Queue` and `Hub` are two asynchronous messaging data types in ZIO that can be converted into the ZIO Stream: ```scala object ZStream { def fromQueue[O]( queue: Dequeue[O], maxChunkSize: Int = DefaultChunkSize ): ZStream[Any, Nothing, O] = ??? def fromHub[A]( hub: Hub[A] ): ZStream[Any, Nothing, A] = ??? } ``` If they contain `Chunk` of elements, we can use `ZStream.fromChunk...` constructors to create a stream from those elements (e.g. `ZStream.fromChunkQueue`): ```scala for { promise <- Promise.make[Nothing, Unit] hub <- Hub.unbounded[Chunk[Int]] scoped = ZStream.fromChunkHubScoped(hub).tap(_ => promise.succeed(())) stream = ZStream.unwrapScoped(scoped) fiber <- stream.foreach(printLine(_)).fork _ <- promise.await _ <- hub.publish(Chunk(1, 2, 3)) _ <- fiber.join } yield () ``` Also, If we need to shutdown a `Queue` or `Hub`, once the stream is closed, we should use `ZStream.from..Shutdown` constructors (e.g. `ZStream.fromQueueWithShutdown`). Also, we can lift a `TQueue` to the ZIO Stream: ```scala for { q <- STM.atomically(TQueue.unbounded[Int]) stream = ZStream.fromTQueue(q) fiber <- stream.foreach(printLine(_)).fork _ <- STM.atomically(q.offer(1)) _ <- STM.atomically(q.offer(2)) _ <- fiber.join } yield () ``` ## From Schedule We can create a stream from a `Schedule` that does not require any further input. The stream will emit an element for each value output from the schedule, continuing for as long as the schedule continues: ```scala val stream: ZStream[Any, Nothing, Long] = ZStream.fromSchedule(Schedule.spaced(1.second) >>> Schedule.recurs(10)) ``` --- ## Error Handling ## Recovering from Failure If we have a stream that may fail, we might need to recover from the failure and run another stream, the `ZStream#orElse` takes another stream, so when the failure occurs it will switch over to the provided stream: ```scala val s1 = ZStream(1, 2, 3) ++ ZStream.fail("Oh! Error!") ++ ZStream(4, 5) val s2 = ZStream(6, 7, 8) val stream = s1.orElse(s2) // Output: 1, 2, 3, 6, 7, 8 ``` Another variant of `orElse` is `ZStream#orElseEither`, which distinguishes elements of the two streams using the `Either` data type. Using this operator, the result of the previous example should be `Left(1), Left(2), Left(3), Right(6), Right(7), Right(8)`. ZIO stream has `ZStream#catchAll` which is powerful version of `ZStream#orElse`. By using `catchAll` we can decide what to do based on the type and value of the failure: ```scala val first = ZStream(1, 2, 3) ++ ZStream.fail("Uh Oh!") ++ ZStream(4, 5) ++ ZStream.fail("Ouch") val second = ZStream(6, 7, 8) val third = ZStream(9, 10, 11) val stream = first.catchAll { case "Uh Oh!" => second case "Ouch" => third } // Output: 1, 2, 3, 6, 7, 8 ``` ## Recovering from Defects If we need to recover from all causes of failures including defects we should use the `ZStream#catchAllCause` method: ```scala val s1 = ZStream(1, 2, 3) ++ ZStream.dieMessage("Oh! Boom!") ++ ZStream(4, 5) val s2 = ZStream(7, 8, 9) val stream = s1.catchAllCause(_ => s2) // Output: 1, 2, 3, 7, 8, 9 ``` ## Recovery from Some Errors If we need to recover from specific failure we should use `ZStream#catchSome`: ```scala val s1 = ZStream(1, 2, 3) ++ ZStream.fail("Oh! Error!") ++ ZStream(4, 5) val s2 = ZStream(7, 8, 9) val stream = s1.catchSome { case "Oh! Error!" => s2 } // Output: 1, 2, 3, 7, 8, 9 ``` And, to recover from a specific cause, we should use `ZStream#catchSomeCause` method: ```scala val s1 = ZStream(1, 2, 3) ++ ZStream.dieMessage("Oh! Boom!") ++ ZStream(4, 5) val s2 = ZStream(7, 8, 9) val stream = s1.catchSomeCause { case Die(value, _) => s2 } ``` ## Recovering to ZIO Effect If our stream encounters an error, we can provide some cleanup task as ZIO effect to our stream by using the `ZStream#onError` method: ```scala val stream = (ZStream(1, 2, 3) ++ ZStream.dieMessage("Oh! Boom!") ++ ZStream(4, 5)) .onError(_ => Console.printLine("Stream application closed! We are doing some cleanup jobs.").orDie) ``` ## Retry a Failing Stream When a stream fails, it can be retried according to the given schedule to the `ZStream#retry` operator: ```scala val numbers = ZStream(1, 2, 3) ++ ZStream .fromZIO( Console.print("Enter a number: ") *> Console.readLine .flatMap(x => x.toIntOption match { case Some(value) => ZIO.succeed(value) case None => ZIO.fail("NaN") } ) ) .retry(Schedule.exponential(1.second)) ``` ## From/To Either Sometimes, we might be working with legacy API which does error handling with the `Either` data type. We can _absolve_ their error types into the ZStream effect using `ZStream.absolve`: ```scala def legacyFetchUrlAPI(url: URL): Either[Throwable, String] = ??? def fetchUrl( url: URL ): ZStream[Any, Throwable, String] = ZStream.fromZIO( ZIO.attemptBlocking(legacyFetchUrlAPI(url)) ).absolve ``` The type of this stream before absolving is `ZStream[Any, Throwable, Either[Throwable, String]]`, this operation let us submerge the error case of an `Either` into the `ZStream` error type. We can do the opposite by exposing an error of type `ZStream[R, E, A]` as a part of the `Either` by using `ZStream#either`: ```scala val inputs: ZStream[Any, Nothing, Either[IOException, String]] = ZStream.fromZIO(Console.readLine).either ``` When we are working with streams of `Either` values, we might want to fail the stream as soon as the emission of the first `Left` value: ```scala // Stream of Either values that cannot fail val eitherStream: ZStream[Any, Nothing, Either[String, Int]] = ZStream(Right(1), Right(2), Left("failed to parse"), Right(4)) // A Fails with the first emission of the left value val stream: ZStream[Any, String, Int] = eitherStream.rightOrFail("fail") ``` ## Refining Errors We can keep one or some errors and terminate the fiber with the rest by using `ZStream#refineOrDie`: ```scala val stream: ZStream[Any, Throwable, Int] = ZStream.fail(new Throwable) val res: ZStream[Any, IllegalArgumentException, Int] = stream.refineOrDie { case e: IllegalArgumentException => e } ``` ## Timing Out We can timeout a stream if it does not produce a value after some duration using `ZStream#timeout`, `ZStream#timeoutFail` and `timeoutFailCause` operators: ```scala stream.timeoutFail(new TimeoutException)(10.seconds) ``` Or we can switch to another stream if the first stream does not produce a value after some duration: ```scala val alternative = ZStream.fromZIO(ZIO.attempt(???)) stream.timeoutTo(10.seconds)(alternative) ``` --- ## Introduction to ZStream A `ZStream[R, E, O]` is a description of a program that, when evaluated, may emit zero or more values of type `O`, may fail with errors of type `E`, and uses an environment of type `R`. One way to think of `ZStream` is as a `ZIO` program that could emit multiple values. As we know, a `ZIO[R, E, A]` data type, is a functional effect which is a description of a program that needs an environment of type `R`, it may end with an error of type `E`, and in case of success, it returns a value of type `A`. The important note about `ZIO` effects is that in the case of success they always end with exactly one value. There is no optionality here, no multiple infinite values, we always get exact value: ```scala val failedEffect: ZIO[Any, String, Nothing] = ZIO.fail("fail!") val oneIntValue : ZIO[Any, Nothing, Int] = ZIO.succeed(3) val oneListValue: ZIO[Any, Nothing, List[Int]] = ZIO.succeed(List(1, 2, 3)) val oneOption : ZIO[Any, Nothing , Option[Int]] = ZIO.succeed(None) ``` A functional stream is pretty similar, it is a description of a program that requires an environment of type `R` and it may signal with errors of type `E` and it yields `O`, but the difference is that it will yield zero or more values. So a `ZStream` represents one of the following cases in terms of its elements: - **An Empty Stream** — It might end up empty; which represent an empty stream, e.g. `ZStream.empty`. - **One Element Stream** — It can represent a stream with just one value, e.g. `ZStream.succeed(3)`. - **Multiple Finite Element Stream** — It can represent a stream of finite values, e.g. `ZStream.range(1, 10)` - **Multiple Infinite Element Stream** — It can even represent a stream that _never ends_ as an infinite stream, e.g. `ZStream.iterate(1)(_ + 1)`. ```scala val emptyStream : ZStream[Any, Nothing, Nothing] = ZStream.empty val oneIntValueStream : ZStream[Any, Nothing, Int] = ZStream.succeed(4) val oneListValueStream : ZStream[Any, Nothing, List[Int]] = ZStream.succeed(List(1, 2, 3)) val finiteIntStream : ZStream[Any, Nothing, Int] = ZStream.range(1, 10) val infiniteIntStream : ZStream[Any, Nothing, Int] = ZStream.iterate(1)(_ + 1) ``` Another example of a stream is when we're pulling a Kafka topic or reading from a socket. There is no inherent definition of an end there. Stream elements arrive at some point, or even they might never arrive at any point. Based on type parameters of `ZStream`, there are 4 types of streams: 1. `ZStream[Any, Nothing, O]` — A stream that emits `O` values and cannot fail. 2. `ZStream[Any, Throwable, O]` — A stream that emits `O` values and can fail with `Throwable`. 3. `ZStream[Any, Nothing, Nothing]` — A stream that emits no elements. 4. `ZStream[R, E, O]` — A stream that requires access to the `R` service, can fail with error of type `E` and emits `O` values. --- ## Operations ## Tapping Tapping is an operation of running an effect on each emission of the ZIO Stream. We can think of `ZStream#tap` as an operation that allows us to observe each element of the stream, do some effectful operation and discard the result of this observation. The `tap` operation does not change elements of the stream, it does not affect the return type of the stream. For example, we can print each element of a stream by using the `tap` operation: ```scala val stream: ZStream[Any, IOException, Int] = ZStream(1, 2, 3) .tap(x => printLine(s"before mapping: $x")) .map(_ * 2) .tap(x => printLine(s"after mapping: $x")) ``` ## Taking Elements We can take a certain number of elements from a stream: ```scala val stream = ZStream.iterate(0)(_ + 1) val s1 = stream.take(5) // Output: 0, 1, 2, 3, 4 val s2 = stream.takeWhile(_ < 5) // Output: 0, 1, 2, 3, 4 val s3 = stream.takeUntil(_ == 5) // Output: 0, 1, 2, 3, 4, 5 val s4 = s3.takeRight(3) // Output: 3, 4, 5 ``` ## Mapping **map** — Applies a given function to all element of this stream to produce another stream: ```scala val intStream: UStream[Int] = ZStream.fromIterable(0 to 100) val stringStream: UStream[String] = intStream.map(_.toString) ``` If our transformation is effectful, we can use `ZStream#mapZIO` instead. **mapZIOPar** — It is similar to `mapZIO`, but will evaluate effects in parallel. It will emit the results downstream in the original order. The `n` argument specifies the number of concurrent running effects. Let's write a simple page downloader, which download URLs concurrently: ```scala def fetchUrl(url: URL): Task[String] = ZIO.succeed(???) def getUrls: Task[List[URL]] = ZIO.succeed(???) val pages = ZStream.fromIterableZIO(getUrls).mapZIOPar(8)(fetchUrl) ``` **mapChunk** — Each stream is backed by some `Chunk`s. By using `mapChunk` we can batch the underlying stream and map every `Chunk` at once: ```scala val chunked = ZStream .fromChunks(Chunk(1, 2, 3), Chunk(4, 5), Chunk(6, 7, 8, 9)) val stream = chunked.mapChunks(x => x.tail) // Input: 1, 2, 3, 4, 5, 6, 7, 8, 9 // Output: 2, 3, 5, 7, 8, 9 ``` If our transformation is effectful we can use `mapChunksZIO` combinator. **mapAccum** — It is similar to a `map`, but it **transforms elements statefully**. `mapAccum` allows us to _map_ and _accumulate_ in the same operation. ```scala abstract class ZStream[-R, +E, +O] { def mapAccum[S, O1](s: S)(f: (S, O) => (S, O1)): ZStream[R, E, O1] } ``` Let's write a transformation, which calculate _running total_ of input stream: ```scala def runningTotal(stream: UStream[Int]): UStream[Int] = stream.mapAccum(0)((acc, next) => (acc + next, acc + next)) // input: 0, 1, 2, 3, 4, 5 // output: 0, 1, 3, 6, 10, 15 ``` **mapConcat** — It is similar to `map`, but maps each element to zero or more elements with the type of `Iterable` and then flattens the whole stream: ```scala val numbers: UStream[Int] = ZStream("1-2-3", "4-5", "6") .mapConcat(_.split("-")) .map(_.toInt) // Input: "1-2-3", "4-5", "6" // Output: 1, 2, 3, 4, 5, 6 ``` The effectful version of `mapConcat` is `mapConcatZIO`. `ZStream` also has chunked versions of that which are `mapConcatChunk` and `mapConcatChunkZIO`. **as** — The `ZStream#as` method maps the success values of this stream to the specified constant value. For example, we can map all element to the unit value: ```scala val unitStream: ZStream[Any, Nothing, Unit] = ZStream.range(1, 5).as(()) ``` ## Filtering The `ZStream#filter` allows us to filter emitted elements: ```scala val s1 = ZStream.range(1, 11).filter(_ % 2 == 0) // Output: 2, 4, 6, 8, 10 // The `ZStream#withFilter` operator enables us to write filter in for-comprehension style val s2 = for { i <- ZStream.range(1, 11).take(10) if i % 2 == 0 } yield i // Output: 2, 4, 6, 8, 10 val s3 = ZStream.range(1, 11).filterNot(_ % 2 == 0) // Output: 1, 3, 5, 7, 9 ``` ## Scanning Scans are like folds, but with a history. Like folds, they take a binary operator with an initial value. A scan combines elements of a stream and emits every intermediary result as an output of the stream: ```scala val scan = ZStream(1, 2, 3, 4, 5).scan(0)(_ + _) // Output: 0, 1, 3, 6, 10, 15 // Iterations: // => 0 (initial value) // 0 + 1 => 1 // 1 + 2 => 3 // 3 + 3 => 6 // 6 + 4 => 10 // 10 + 5 => 15 val fold = ZStream(1, 2, 3, 4, 5).runFold(0)(_ + _) // Output: 15 (ZIO effect containing 15) ``` ## Draining Assume we have an effectful stream, which contains a sequence of effects; sometimes we might want to execute its effect without emitting any element, in these situations to discard the results we should use the `ZStream#drain` method. It removes all output values from the stream: ```scala val s1: ZStream[Any, Nothing, Nothing] = ZStream(1, 2, 3, 4, 5).drain // Emitted Elements: val s2: ZStream[Any, IOException, Int] = ZStream .repeatZIO { for { nextInt <- Random.nextInt number = Math.abs(nextInt % 10) _ <- Console.printLine(s"random number: $number") } yield (number) } .take(3) // Emitted Elements: 1, 4, 7 // Result of Stream Effect on the Console: // random number: 1 // random number: 4 // random number: 7 val s3: ZStream[Any, IOException, Nothing] = s2.drain // Emitted Elements: // Result of Stream Effect on the Console: // random number: 4 // random number: 8 // random number: 2 ``` The `ZStream#drain` often used with `ZStream#merge` to run one side of the merge for its effect while getting outputs from the opposite side of the merge: ```scala val logging = ZStream.fromZIO( printLine("Starting to merge with the next stream") ) val stream = ZStream(1, 2, 3) ++ logging.drain ++ ZStream(4, 5, 6) // Emitted Elements: 1, 2, 3, 4, 5, 6 // Result of Stream Effect on the Console: // Starting to merge with the next stream ``` Note that if we do not drain the `logging` stream, the emitted elements would be contained unit value: ```scala val stream = ZStream(1, 2, 3) ++ logging ++ ZStream(4, 5, 6) // Emitted Elements: 1, 2, 3, (), 4, 5, 6 // Result of Stream Effect on the Console: // Starting to merge with the next stream ``` ## Changes The `ZStream#changes` emits elements that are not equal to the previous element: ```scala val changes = ZStream(1, 1, 1, 2, 2, 3, 4).changes // Output: 1, 2, 3, 4 ``` The `ZStream#changes` operator, uses natural equality to determine whether two elements are equal. If we prefer the specialized equality checking, we can provide a function of type `(O, O) => Boolean` to the `ZStream#changesWith` operator. Assume we have a stream of events with a composite key of _partition_ and _offset_ attributes, and we know that the offset is monotonic in each partition. So, we can use the `changesWith` operator to create a stream of unique elements: ```scala case class Event(partition: Long, offset: Long, metadata: String) val events: ZStream[Any, Nothing, Event] = ZStream.fromIterable(???) val uniques = events.changesWith((e1, e2) => (e1.partition == e2.partition && e1.offset == e2.offset)) ``` ## Collecting We can perform `filter` and `map` operations in a single step using the `ZStream#collect` operation: ```scala val source1 = ZStream(1, 2, 3, 4, 0, 5, 6, 7, 8) val s1 = source1.collect { case x if x < 6 => x * 2 } // Output: 2, 4, 6, 8, 0, 10 val s2 = source1.collectWhile { case x if x != 0 => x * 2 } // Output: 2, 4, 6, 8 val source2 = ZStream(Left(1), Right(2), Right(3), Left(4), Right(5)) val s3 = source2.collectLeft // Output: 1, 4 val s4 = source2.collectWhileLeft // Output: 1 val s5 = source2.collectRight // Output: 2, 3, 5 val s6 = source2.drop(1).collectWhileRight // Output: 2, 3 val s7 = source2.map(_.toOption).collectSome // Output: 2, 3, 5 val s8 = source2.map(_.toOption).collectWhileSome // Output: empty stream ``` We can also do effectful collect using `ZStream#collectZIO` and `ZStream#collectWhileZIO`. ZIO stream has `ZStream#collectSuccess` which helps us to perform effectful operations and just collect the success values: ```scala val urls = ZStream( "dotty.epfl.ch", "zio.dev", "zio.github.io/zio-json", "zio.github.io/zio-nio/" ) def fetch(url: String): ZIO[Any, Throwable, String] = ZIO.attemptBlocking(???) val pages = urls .mapZIO(url => fetch(url).exit) .collectSuccess ``` ## Zipping We can zip two stream by using `ZStream.zip` or `ZStream#zipWith` operator: ```scala val s1: UStream[(Int, String)] = ZStream(1, 2, 3, 4, 5, 6).zipWith(ZStream("a", "b", "c"))((a, b) => (a, b)) val s2: UStream[(Int, String)] = ZStream(1, 2, 3, 4, 5, 6).zip(ZStream("a", "b", "c")) // Output: (1, "a"), (2, "b"), (3, "c") ``` The new stream will end when one of the streams ends. In case of ending one stream before another, we might need to zip with default values; the `ZStream#zipAll` or `ZStream#zipAllWith` takes default values of both sides to perform such mechanism for us: ```scala val s1 = ZStream(1, 2, 3) .zipAll(ZStream("a", "b", "c", "d", "e"))(0, "x") val s2 = ZStream(1, 2, 3).zipAllWith( ZStream("a", "b", "c", "d", "e") )(_ => 0, _ => "x")((a, b) => (a, b)) // Output: (1, a), (2, b), (3, c), (0, d), (0, e) ``` Sometimes we want to zip streams, but do not want to zip two elements one by one. For example, we may have two streams producing elements at different speeds, and do not want to wait for the slower one when zipping elements. When we need to zip elements with the latest element of the slower stream, `ZStream#zipLatest` or `ZStream#zipLatestWith` will do this for us. It zips two streams so that when a value is emitted by either of the two streams, it is combined with the latest value from the other stream to produce a result: ```scala val s1 = ZStream(1, 2, 3) .schedule(Schedule.spaced(1.second)) val s2 = ZStream("a", "b", "c", "d") .schedule(Schedule.spaced(500.milliseconds)) .rechunk(3) s1.zipLatest(s2) // Output: (1, a), (1, b), (1, c), (1, d), (2, d), (3, d) ``` ZIO Stream also has three useful operators for zipping element of a stream with their previous/next elements and also both of them: ```scala val stream: UStream[Int] = ZStream.fromIterable(1 to 5) val s1: UStream[(Option[Int], Int)] = stream.zipWithPrevious val s2: UStream[(Int, Option[Int])] = stream.zipWithNext val s3: UStream[(Option[Int], Int, Option[Int])] = stream.zipWithPreviousAndNext ``` By using `ZStream#zipWithIndex` we can index elements of a stream: ```scala val indexedStream: ZStream[Any, Nothing, (String, Long)] = ZStream("Mary", "James", "Robert", "Patricia").zipWithIndex // Output: ("Mary", 0L), ("James", 1L), ("Robert", 2L), ("Patricia", 3L) ``` ## Cross Product ZIO stream has `ZStream#cross` and its variants to compute _Cartesian Product_ of two streams: ```scala val first = ZStream(1, 2, 3) val second = ZStream("a", "b") val s1 = first cross second val s2 = first <*> second val s3 = first.crossWith(second)((a, b) => (a, b)) // Output: (1,a), (1,b), (2,a), (2,b), (3,a), (3,b) val s4 = first crossLeft second val s5 = first <* second // Keep only elements from the left stream // Output: 1, 1, 2, 2, 3, 3 val s6 = first crossRight second val s7 = first *> second // Keep only elements from the right stream // Output: a, b, a, b, a, b ``` Note that the right-hand side stream would be run multiple times, for every element in the left stream. ZIO stream also has `ZStream.crossN` which takes streams up to four one. ## Partitioning ### partition `ZStream#partition` function splits the stream into tuple of streams based on the predicate. The first stream contains all element evaluated to true, and the second one contains all element evaluated to false. The faster stream may advance by up to `buffer` elements further than the slower one. Two streams are wrapped by `Scope` type. In the example below, left stream consists of even numbers only: ```scala val partitionResult: ZIO[Scope, Nothing, (ZStream[Any, Nothing, Int], ZStream[Any, Nothing, Int])] = ZStream .fromIterable(0 to 100) .partition(_ % 2 == 0, buffer = 50) ``` ### partitionEither If we need to partition a stream using an effectful predicate we can use `ZStream.partitionEither`. ```scala abstract class ZStream[-R, +E, +O] { final def partitionEither[R1 <: R, E1 >: E, O2, O3]( p: O => ZIO[R1, E1, Either[O2, O3]], buffer: Int = 16 ): ZIO[R1 with Scope, E1, (ZStream[Any, E1, O2], ZStream[Any, E1, O3])] } ``` Here is a simple example of using this function: ```scala val partitioned: ZIO[Scope, Nothing, (ZStream[Any, Nothing, Int], ZStream[Any, Nothing, Int])] = ZStream .fromIterable(1 to 10) .partitionEither(x => ZIO.succeed(if (x < 5) Left(x) else Right(x))) ``` ## GroupBy ### groupByKey To partition the stream by function result we can use `groupBy` by providing a function of type `O => K` which determines by which keys the stream should be partitioned. ```scala abstract class ZStream[-R, +E, +O] { final def groupByKey[K]( f: O => K, buffer: Int = 16 ): ZStream.GroupBy[R, E, K, O] } ``` In the example below, exam results are grouped into buckets and counted: ```scala case class Exam(person: String, score: Int) val examResults = Seq( Exam("Alex", 64), Exam("Michael", 97), Exam("Bill", 77), Exam("John", 78), Exam("Bobby", 71) ) val groupByKeyResult: ZStream[Any, Nothing, (Int, Int)] = ZStream .fromIterable(examResults) .groupByKey(exam => exam.score / 10 * 10) { case (k, s) => ZStream.fromZIO(s.runCollect.map(l => k -> l.size)) } ``` :::note `groupByKey` partition the stream by a simple function of type `O => K`; It is not an effectful function. In some cases we need to partition the stream by using an _effectful function_ of type `O => ZIO[R1, E1, (K, V)]`; So we can use `groupBy` which is the powerful version of `groupByKey` function. ::: ### groupBy It takes an effectful function of type `O => ZIO[R1, E1, (K, V)]`; ZIO Stream uses this function to partition the stream and gives us a new data type called `ZStream.GroupBy` which represent a grouped stream. `GroupBy` has an `apply` method, that takes a function of type `(K, ZStream[Any, E, V]) => ZStream[R1, E1, A]`; ZIO Runtime runs this function across all groups and then merges them in a non-deterministic fashion as a result. ```scala abstract class ZStream[-R, +E, +O] { final def groupBy[R1 <: R, E1 >: E, K, V]( f: O => ZIO[R1, E1, (K, V)], buffer: Int = 16 ): ZStream.GroupBy[R1, E1, K, V] } ``` In the example below, we are going `groupBy` given names by their first character and then count the number of names in each group: ```scala val counted: UStream[(Char, Long)] = ZStream("Mary", "James", "Robert", "Patricia", "John", "Jennifer", "Rebecca", "Peter") .groupBy(x => ZIO.succeed((x.head, x))) { case (char, stream) => ZStream.fromZIO(stream.runCount.map(count => char -> count)) } // Input: Mary, James, Robert, Patricia, John, Jennifer, Rebecca, Peter // Output: (P, 2), (R, 2), (M, 1), (J, 3) ``` Let's change the above example a bit into an example of classifying students. The teacher assigns the student to a specific class based on the student's talent. Note that the partitioning operation is an effectful: ```scala val classifyStudents: ZStream[Any, IOException, (String, Seq[String])] = ZStream.fromZIO( printLine("Please assign each student to one of the A, B, or C classrooms.") ) *> ZStream("Mary", "James", "Robert", "Patricia", "John", "Jennifer", "Rebecca", "Peter") .groupBy(student => printLine(s"What is the classroom of $student? ") *> readLine.map(classroom => (classroom, student)) ) { case (classroom, students) => ZStream.fromZIO( students .runFold(Seq.empty[String])((s, e) => s :+ e) .map(students => classroom -> students) ) } // Input: // Please assign each student to one of the A, B, or C classrooms. // What is the classroom of Mary? A // What is the classroom of James? B // What is the classroom of Robert? A // What is the classroom of Patricia? C // What is the classroom of John? B // What is the classroom of Jennifer? A // What is the classroom of Rebecca? C // What is the classroom of Peter? A // // Output: // (B,List(James, John)) // (A,List(Mary, Robert, Jennifer, Peter)) // (C,List(Patricia, Rebecca)) ``` ## Grouping ### grouped To partition the stream results with the specified chunk size, we can use the `grouped` function. ```scala val groupedResult: ZStream[Any, Nothing, Chunk[Int]] = ZStream.fromIterable(0 to 8).grouped(3) // Input: 0, 1, 2, 3, 4, 5, 6, 7, 8 // Output: Chunk(0, 1, 2), Chunk(3, 4, 5), Chunk(6, 7, 8) ``` ### groupedWithin It allows grouping events by time or chunk size, whichever is satisfied first. In the example below every chunk consists of 30 elements and is produced every 3 seconds. ```scala val groupedWithinResult: ZStream[Any, Nothing, Chunk[Int]] = ZStream.fromIterable(0 to 10) .repeat(Schedule.spaced(1.seconds)) .groupedWithin(30, 10.seconds) ``` ## Concatenation We can concatenate two streams by using `ZStream#++` or `ZStream#concat` operator which returns a stream that emits the elements from the left-hand stream and then emits the elements from the right stream: ```scala silent:nest val a = ZStream(1, 2, 3) val b = ZStream(4, 5) val c1 = a ++ b val c2 = a concat b ``` Also, we can use `ZStream.concatAll` constructor to concatenate given streams together: ```scala val c3 = ZStream.concatAll(Chunk(a, b)) ``` There is also the `ZStream#flatMap` combinator which create a stream which elements are generated by applying a function of type `O => ZStream[R1, E1, O2]` to each output of the source stream and concatenated all of the results: ```scala val stream = ZStream(1, 2, 3).flatMap(x => ZStream.repeat(x).take(4)) // Input: 1, 2, 3 // Output: 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3 ``` Assume we have an API that takes an author name and returns all its book: ```scala def getAuthorBooks(author: String): ZStream[Any, Throwable, Book] = ZStream(???) ``` If we have a stream of author's names, we can use `ZStream#flatMap` to concatenate the results of all API calls: ```scala val authors: ZStream[Any, Throwable, String] = ZStream("Mary", "James", "Robert", "Patricia", "John") val allBooks: ZStream[Any, Throwable, Book] = authors.flatMap(getAuthorBooks _) ``` If we need to do the `flatMap` concurrently, we can use `ZStream#flatMapPar`, and also if the order of concatenation is not important for us, we can use the `ZStream#flatMapParSwitch` operator. ## Merging Sometimes we need to interleave the emission of two streams and create another stream. In these cases, we can't use the `ZStream.concat` operation because the `concat` operation waits for the first stream to finish and then consumes the second stream. So we need a non-deterministic way of picking elements from different sources. ZIO Stream's `merge` operations does this for us. Let's discuss some variants of this operation: ### merge The `ZSstream#merge` picks elements randomly from specified streams: ```scala val s1 = ZStream(1, 2, 3).rechunk(1) val s2 = ZStream(4, 5, 6).rechunk(1) val merged = s1 merge s2 // As the merge operation is not deterministic, it may output the following stream of numbers: // Output: 4, 1, 2, 5, 6, 3 ``` Merge operation always try to pull one chunk from each stream, if we chunk our streams equal or over 3 elements in the last example, we encounter a new stream containing one of the `1, 2, 3, 4, 5, 6` or `4, 5, 6, 1, 2, 3` elements. ### Termination Strategy When we merge two streams, we should think about the _termination strategy_ of this operation. Each stream has a specific lifetime. One stream may emit all its elements and finish its job, another stream may end after one hour of emission, one another may have a long-running lifetime and never end. So when we merge two streams with different lifetimes, what is the termination strategy of the resulting stream? By default, when we merge two streams using `ZStream#merge` operation, the newly produced stream will terminate when both specified streams terminate. We can also define the _termination strategy_ corresponding to our requirement. ZIO Stream supports four different termination strategies: - **Left** — The resulting stream will terminate when the left-hand side stream terminates. - **Right** — The resulting stream will terminate when the right-hand side stream finishes. - **Both** — The resulting stream will terminate when both streams finish. - **Either** — The resulting stream will terminate when one of the streams finishes. Here is an example of specifying termination strategy when merging two streams: ```scala val s1 = ZStream.iterate(1)(_+1).take(5).rechunk(1) val s2 = ZStream.repeat(0).rechunk(1) val merged = s1.merge(s2, HaltStrategy.Left) ``` We can also use `ZStream#mergeTerminateLeft`, `ZStream#mergeTerminateRight` or `ZStream#mergeTerminateEither` operations instead of specifying manually the termination strategy. ### mergeAll Usually, micro-services or long-running applications are composed of multiple components that need to run infinitely in the background and if something happens to them, or they terminate abruptly we should crash the entire application. So our main fiber should perform these three things: * **Launch and wait** — It should launch all of those background components and wait infinitely. It should not exit prematurely, because then our application won't be running. * **Interrupt everything** — It should interrupt all those components whenever we receive a termination signal from the operating system. * **Watch all fibers** — It should watch all those fibers (background components), and quickly exit if something goes wrong. So how should we do that with our main fiber? Let's try to create a long-running application: ```scala val main = kafkaConsumer.runDrain.fork *> httpServer.fork *> scheduledJobRunner.fork *> ZIO.never ``` We can launch the Kafka consumer, the HTTP server, and our job runner and fork them, and then wait using `ZIO.never`. This will indeed wait, but if something happens to any of them and if they crash, nothing happens. So our application just hangs and remains up without anything working in the background. So this approach does not work properly. So another idea is to watch background components. By using `ZIO.raceFirst` as soon as one of those fibers terminates with either success or failure, it will interrupt all the rest of the components: ```scala val main = ZIO.raceFirst(kafkaConsumer.runDrain, List(httpServer, scheduledJobRunner)) ``` We can also do this with streams: ```scala val main = for { //_ <- other resources _ <- ZStream .mergeAllUnbounded(16)( kafkaConsumer.drain, ZStream.fromZIO(httpServer), ZStream.fromZIO(scheduledJobRunner) ) .runDrain } yield () ``` Using `ZStream.mergeAll` we can combine all these streaming components concurrently into one application. ### mergeWith Sometimes we need to merge two streams and after that, unify them and convert them to new element types. We can do this by using the `ZStream#mergeWith` operation: ```scala val s1 = ZStream("1", "2", "3") val s2 = ZStream(4.1, 5.3, 6.2) val merged = s1.mergeWith(s2)(_.toInt, _.toInt) ``` ## Interleaving When we `merge` two streams, the ZIO Stream picks elements from two streams randomly. But how to merge two streams deterministically? The answer is the `ZStream#interleave` operation. The `ZStream#interleave` operator pulls an element from each stream, one by one, and then returns an interleaved stream. When one stream is exhausted, all remaining values in the other stream will be pulled: ```scala val s1 = ZStream(1, 2, 3) val s2 = ZStream(4, 5, 6, 7, 8) val interleaved = s1 interleave s2 // Output: 1, 4, 2, 5, 3, 6, 7, 8 ``` ZIO Stream also has the `interleaveWith` operator, which is a more powerful version of `interleave`. By using `ZStream#interleaveWith`, we can specify the logic of interleaving: ```scala val s1 = ZStream(1, 3, 5, 7, 9) val s2 = ZStream(2, 4, 6, 8, 10) val interleaved = s1.interleaveWith(s2)(ZStream(true, false, false).forever) // Output: 1, 2, 4, 3, 6, 8, 5, 10, 7, 9 ``` `ZStream#interleaveWith` uses a stream of boolean to decide which stream to choose. If it reaches a true value, it will pick a value from the left-hand side stream, otherwise, it will pick from the right-hand side. ## Interspersing We can intersperse any stream by using `ZStream#intersperse` operator: ```scala val s1 = ZStream(1, 2, 3, 4, 5).intersperse(0) // Output: 1, 0, 2, 0, 3, 0, 4, 0, 5 val s2 = ZStream("a", "b", "c", "d").intersperse("[", "-", "]") // Output: [, -, a, -, b, -, c, -, d] ``` ## Broadcasting We can broadcast a stream by using `ZStream#broadcast`, it returns a scoped list of streams that have the same elements as the source stream. The `broadcast` operation emits each element to the inputs of returning streams. The upstream stream can emit events as much as `maximumLag`, then it decreases its speed by the slowest downstream stream. In the following example, we are broadcasting stream of random numbers to the two downstream streams. One of them is responsible to compute the maximum number, and the other one does some logging job with additional delay. The upstream stream decreases its speed by the logging stream: ```scala val stream: ZIO[Any, IOException, Unit] = ZIO.scoped { ZStream .fromIterable(1 to 20) .mapZIO(_ => Random.nextInt) .map(Math.abs) .map(_ % 100) .tap(e => printLine(s"Emit $e element before broadcasting")) .broadcast(2, 5) .flatMap { streams => for { out1 <- streams(0).runFold(0)((acc, e) => Math.max(acc, e)) .flatMap(x => printLine(s"Maximum: $x")) .fork out2 <- streams(1).schedule(Schedule.spaced(1.second)) .foreach(x => printLine(s"Logging to the Console: $x")) .fork _ <- out1.join.zipPar(out2.join) } yield () } } ``` ## Distribution The `ZStream#distributedWith` operator is a more powerful version of `ZStream#broadcast`. It takes a `decide` function, and based on that decide how to distribute incoming elements into the downstream streams: ```scala abstract class ZStream[-R, +E, +O] { final def distributedWith[E1 >: E]( n: Int, maximumLag: Int, decide: O => UIO[Int => Boolean] ): ZIO[R with Scope, Nothing, List[Dequeue[Exit[Option[E1], O]]]] = ??? } ``` In the example below, we are partitioning incoming elements into three streams using `ZStream#distributedWith` operator: ```scala val partitioned: ZIO[Scope, Nothing, (UStream[Int], UStream[Int], UStream[Int])] = ZStream .iterate(1)(_ + 1) .schedule(Schedule.fixed(1.seconds)) .distributedWith(3, 10, x => ZIO.succeed(q => x % 3 == q)) .flatMap { case q1 :: q2 :: q3 :: Nil => ZIO.succeed( ZStream.fromQueue(q1).flattenExitOption, ZStream.fromQueue(q2).flattenExitOption, ZStream.fromQueue(q3).flattenExitOption ) case _ => ZIO.dieMessage("Impossible!") } ``` ## Buffering Since the ZIO streams are pull-based, it means the consumers do not need to message the upstream to slow down. Whenever a downstream stream pulls a new element, the upstream produces a new element. So, the upstream stream is as fast as the slowest downstream stream. Sometimes we need to run producer and consumer independently, in such a situation we can use an asynchronous non-blocking queue for communication between faster producer and slower consumer; the queue can buffer elements between two streams. ZIO stream also has a built-in `ZStream#buffer` operator which does the same thing for us. The `ZStream#buffer` allows a faster producer to progress independently of a slower consumer by buffering up to `capacity` chunks in a queue. In the following example, we are going to buffer a stream. We print each element to the console as they are emitting before and after the buffering: ```scala ZStream .fromIterable(1 to 10) .rechunk(1) .tap(x => Console.printLine(s"before buffering: $x")) .buffer(4) .tap(x => Console.printLine(s"after buffering: $x")) .schedule(Schedule.spaced(5.second)) ``` We spaced 5 seconds between each emission to show the lag between producing and consuming messages. Based on the type of underlying queue we can use one the buffering operators: - **Bounded Queue** — `ZStream#buffer(capacity: Int)` - **Unbounded Queue** — `ZStream#bufferUnbounded` - **Sliding Queue** — `ZStream#bufferSliding(capacity: Int)` - **Dropping Queue** `ZStream#bufferDropping(capacity: Int)` ## Debouncing The `ZStream#debounce` method debounces the stream with a minimum period of `d` between each element: ```scala val stream = ( ZStream(1, 2, 3) ++ ZStream.fromZIO(ZIO.sleep(500.millis)) ++ ZStream(4, 5) ++ ZStream.fromZIO(ZIO.sleep(10.millis)) ++ ZStream(6) ).debounce(100.millis) // emit only after a pause of at least 100 ms // Output: 3, 6 ``` ## Aggregation Aggregation is the process of converting one or more elements of type `A` into elements of type `B`. This operation takes a transducer as an aggregation unit and returns another stream that is aggregated. We have two types of aggregation: ### Synchronous Aggregation They are synchronous because the upstream emits an element when the _transducer_ emits one. To apply a synchronous aggregation to the stream we can use `ZStream#aggregate` or `ZStream#transduce` operations. Let's see an example of synchronous aggregation: ```scala val stream = ZStream(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) val s1 = stream.transduce(ZSink.collectAllN[Int](3)) // Output Chunk(1,2,3), Chunk(4,5,6), Chunk(7,8,9), Chunk(10) ``` Sometimes stream processing element by element is not efficient, specially when we are working with files or doing I/O works; so we might need to aggregate them and process them in a batch way: ```scala val source = ZStream .iterate(1)(_ + 1) .take(200) .tap(x => printLine(s"Producing Element $x") .schedule(Schedule.duration(1.second).jittered) ) val sink = ZSink.foreach((e: Chunk[Int]) => printLine(s"Processing batch of events: $e") .schedule(Schedule.duration(3.seconds).jittered) ) val myApp = source.transduce(ZSink.collectAllN[Int](5)).run(sink) ``` Let's see one output of running this program: ``` Producing element 1 Producing element 2 Producing element 3 Producing element 4 Producing element 5 Processing batch of events: Chunk(1,2,3,4,5) Producing element 6 Producing element 7 Producing element 8 Producing element 9 Producing element 10 Processing batch of events: Chunk(6,7,8,9,10) Producing element 11 Producing element 12 Processing batch of events: Chunk(11,12) ``` Elements are grouped into Chunks of 5 elements and then processed in a batch way. ### Asynchronous Aggregation Asynchronous aggregations, aggregate elements of upstream as long as the downstream operators are busy. To apply an asynchronous aggregation to the stream, we can use `ZStream#aggregateAsync`, `ZStream#aggregateAsyncWithin`, and `ZStream#aggregateAsyncWithinEither` operations. For example, consider `source.aggregateAsync(ZSink.collectAllN[Nothing, Int](5)).mapZIO(processChunks)`. Whenever the downstream (`mapZIO(processChunks)`) is ready for consumption and pulls the upstream, the transducer `(ZTransducer.collectAllN(5))` will flush out its buffer, regardless of whether the `collectAllN` buffered all its 5 elements or not. So the `ZStream#aggregateAsync` will emit when downstream pulls: ```scala val myApp = source.aggregateAsync(ZSink.collectAllN[Int](5)).run(sink) ``` Let's see one output of running this program: ``` Producing element 1 Producing element 2 Producing element 3 Producing element 4 Processing batch of events: Chunk(1,2) Processing batch of events: Chunk(3,4) Producing element 5 Processing batch of events: Chunk(5) Producing element 6 Processing batch of events: Chunk(6) Producing element 7 Producing element 8 Producing element 9 Processing batch of events: Chunk(7) Producing element 10 Producing element 11 Processing batch of events: Chunk(8,9) Producing element 12 Processing batch of events: Chunk(10,11) Processing batch of events: Chunk(12) ``` The `ZStream#aggregateAsyncWithin` is another aggregator which takes a scheduler. This scheduler will consume all events produced by the given transducer. So the `aggregateAsyncWithin` will emit when the transducer emits or when the scheduler expires: ```scala abstract class ZStream[-R, +E, +O] { final def aggregateAsyncWithin[R1 <: R, E1 >: E, E2, A1 >: A, B]( sink: ZSink[R1, E1, A1, E2, A1, B], schedule: Schedule[R1, Option[B], Any] )(implicit trace: Trace): ZStream[R1, E2, B] = ??? } ``` When we are doing I/O, batching is very important. With ZIO streams, we can create user-defined batches. It is pretty easy to do that with the `ZStream#aggregateAsyncWithin` operator. Let's see the below snippet code: ```scala dataStream.aggregateAsyncWithin( ZSink.collectAllN[Record](2000), Schedule.fixed(30.seconds) ) ``` So it will collect elements into a chunk up to 2000 elements and if we have got less than 2000 elements and 30 seconds have passed, it will pass currently collected elements down the stream whether it has collected zero, one, or 2000 elements. So this is a sort of timeout for aggregation operation. This approach aggressively favors **throughput** over **latency**. It will introduce a fixed amount of latency into a stream. We will always wait for up to 30 seconds if we haven't reached this sort of boundary value. Instead, thanks to `Schedule` we can create a much smarter **adaptive batching algorithm** that can balance between **throughput** and **latency*. So what we are doing here is that we are creating a schedule that operates on chunks of records. What the `Schedule` does is that it starts off with 30-second timeouts for as long as its input has a size that is lower than 1000, now once we see an input that has a size look higher than 1000, we will switch to a second schedule with some jittery, and we will remain with this schedule for as long as the batch size is over 1000: ```scala val schedule: Schedule[Any, Option[Chunk[Record]], Long] = // Start off with 30-second timeouts as long as the batch size is < 1000 Schedule.fixed(30.seconds).whileInput[Option[Chunk[Record]]](_.getOrElse(Chunk.empty).length < 100) andThen // and then, switch to a shorter jittered schedule for as long as batches remain over 1000 Schedule.fixed(5.seconds).jittered.whileInput[Option[Chunk[Record]]](_.getOrElse(Chunk.empty).length >= 1000) dataStream .aggregateAsyncWithin(ZSink.collectAllN[Record](2000), schedule) ``` --- ## Resourceful Streams Most of the constructors of `ZStream` have a special variant to lift a scoped resource to a Stream (e.g. `ZStream.fromReaderScoped`). By using these constructors, we are creating streams that are resource-safe. Before creating a stream, they acquire the resource, and after usage; they close the stream. ZIO Stream also has `acquireRelease` and `finalizer` constructors which are similar to `ZIO.acquireRelease`. They allow us to clean up or finalizing before the stream ends: ## Acquire Release We can provide `acquire` and `release` actions to `ZStream.acquireReleaseWith` to create a resourceful stream: ```scala object ZStream { def acquireReleaseWith[R, E, A]( acquire: ZIO[R, E, A] )( release: A => URIO[R, Any] ): ZStream[R, E, A] = ??? ``` Let's see an example of using an acquire release when reading a file. In this example, by providing `acquire` and `release` actions to `ZStream.acquireReleaseWith`, it gives us a scoped stream of `BufferedSource`. As this stream is scoped, we can convert that `BufferedSource` to a stream of its lines and then run it, without worrying about resource leakage: ```scala val lines: ZStream[Any, Throwable, String] = ZStream .acquireReleaseWith( ZIO.attempt(Source.fromFile("file.txt")) <* printLine("The file was opened.") )(x => ZIO.succeed(x.close()) <* printLine("The file was closed.").orDie) .flatMap { is => ZStream.fromIterator(is.getLines()) } ``` ## Finalization We can also create a stream that never fails and define a finalizer for it, so that finalizer will be executed before that stream ends. ```scala object ZStream { def finalizer[R]( finalizer: URIO[R, Any] ): ZStream[R, Nothing, Any] = ??? } ``` It is useful when need to add a finalizer to an existing stream. Assume we need to clean up the temporary directory after our streaming application ends: ```scala def application: ZStream[Any, IOException, Unit] = ZStream.fromZIO(printLine("Application Logic.")) def deleteDir(dir: Path): ZIO[Any, IOException, Unit] = printLine("Deleting file.") val myApp: ZStream[Any, IOException, Any] = application ++ ZStream.finalizer( (deleteDir(Paths.get("tmp")) *> printLine("Temporary directory was deleted.")).orDie ) ``` ## Ensuring We might want to run some code after the execution of the stream's finalization. To do so, we can use the `ZStream#ensuring` operator: ```scala ZStream .finalizer(Console.printLine("Finalizing the stream").orDie) .ensuring( printLine("Doing some other works after stream's finalization").orDie ) // Output: // Finalizing the stream // Doing some other works after stream's finalization ``` --- ## Scheduling To schedule the output of a stream we use `ZStream#schedule` combinator. Let's space between each emission of the given stream: ```scala val stream = ZStream(1, 2, 3, 4, 5).schedule(Schedule.spaced(1.second)) ``` --- ## Streams Are Chunked by Default Every time we are working with streams, we are always working with chunks. There are no streams with individual elements, these streams have always chunks in their underlying implementation. So every time we evaluate a stream, when we pull an element out of a stream, we are actually pulling out a chunk of elements. So why streams are designed in this way? This is because of the **efficiency and performance** issues. Every I/O operation in the programming world works with batches. We never work with a single element. For example, whenever we are reading or writing from/to a file descriptor, or a socket we are reading or writing multiple elements at a time. This is also true when we are working with an HTTP server or even JDBC drivers. We always read and write multiple bytes to be more performant. So let's talk a bit about Chunk. Chunk is a ZIOs immutable array-backed collection. It is initially written for ZIO stream, but later it has been evolved into a very attractive general collection type which is also useful for other purposes. The `Chunk` data type is an immutable array-backed collection. Most importantly it tries to keep primitives unboxed. This is super important for the efficient processing of files and sockets. They are also very useful and efficient for encoding and decoding and writing transducers. To learn more about this data type, we have introduced that at the [Chunk](../chunk.md) section. --- ## Type Aliases The `ZStream` data type, has two type aliases: ```scala type Stream[+E, +A] = ZStream[Any, E, A] type UStream[+A] = ZStream[Any, Nothing, A] ``` 1. `Stream[E, A]` is a type alias for `ZStream[Any, E, A]`, which represents a ZIO stream that does not require any services, and may fail with an `E`, or produce elements with an `A`. 2. `UStream[A]` is a type alias for `ZStream[Any, Nothing, A]`, which represents a ZIO stream that does not require any services, it cannot fail, and after evaluation, it may emit zero or more values of type `A`. --- ## ConcurrentMap A `ConcurrentMap` is a wrapper over `java.util.concurrent.ConcurrentHashMap`. ## Motivation The `HashMap` in the Scala standard library is not thread-safe. This means that if multiple fibers are accessing the same key, and trying to modify the value, this can lead to inconsistent results. For example, assume we have a `HashMap` with a key `foo` and a value of `0`. Let's see what happens if we perform the `inc` workflow 100 times concurrently: ```scala object MainApp extends ZIOAppDefault { def inc(ref: Ref[mutable.HashMap[String, Int]], key: String) = for { _ <- ref.get _ <- ref.update { map => map.updateWith(key)(_.map(_ + 1)) map } } yield () def run = for { ref <- Ref.make(mutable.HashMap(("foo", 0))) _ <- ZIO.foreachParDiscard(1 to 100)(_ => inc(ref, "foo")) _ <- ref.get.map(_.get("foo")).debug("The final value of foo is") } yield () } // Different outputs on different executions: // The final value of foo is Some(72) // The final value of foo is Some(84) // The final value of foo is Some(78) // ... ``` Since the `HashMap` is not thread-safe, every time we run this program, we might get different results, which is not desirable. So we need a concurrent data structure that can be used safely in concurrent environments, which the `ConcurrentMap` does for us: ```scala object MainApp extends ZIOAppDefault { def run = for { map <- ConcurrentMap.make(("foo", 0), ("bar", 1), ("baz", 2)) _ <- ZIO.foreachParDiscard(1 to 100)(_ => map.computeIfPresent("foo", (_, v) => v + 1) ) _ <- map.get("foo").debug("The final value of foo is") } yield () } // Output: // The final value of foo is Some(100) ``` ## Creation To make an empty `ConcurrentMap` we use `ConcurrentMap.empty`: ```scala val empty = ConcurrentMap.empty[String, Int] ``` And to make a `ConcurrentMap` with some initial values we use `ConcurrentMap.make` or `ConcurrentMap.fromIterable`: ```scala val map1 = ConcurrentMap.make(("foo", 0), ("bar", 1), ("baz", 2)) val map2 = ConcurrentMap.fromIterable(List(("foo", 0), ("bar", 1), ("baz", 2))) ``` ## Update Operations Basic operations are provided to manipulate the values in the `ConcurrentMap`: ### Putting values | Method | Definition | |-------------------------------------------------|------------------------------------------------------------------------------------------------------------| | `put(key: K, value: V): UIO[Option[V]]` | Adds a new key-value pair and optionally returns previously bound value. | | `putAll(keyValues: (K, V)*): UIO[Unit]` | Adds all new key-value pairs. | | | `putIfAbsent(key: K, value: V): UIO[Option[V]]` | Adds a new key-value pair, unless the key is already bound to some other value. | ### Removing values | Method | Definition | |---------------------------------------------|------------------------------------------------------------------------------------------------------------| | `remove(key: K): UIO[Option[V]]` | Removes the entry for the given key, optionally returning value associated with it. | | `remove(key: K, value: V): UIO[Boolean]` | Removes the entry for the given key if it is mapped to a given value. | | `removeIf(p: (K, V) => Boolean): UIO[Unit]` | Removes all elements which do not satisfy the given predicate. | | `retainIf(p: (K, V) => Boolean): UIO[Unit]` | Removes all elements which do not satisfy the given predicate. | | `clear: UIO[Unit]` | Removes all elements. | ### Replacing values | Method | Definition | |-----------------------------------------------------------|------------------------------------------------------------------------------------------------------------| | `replace(key: K, value: V): UIO[Option[V]]` | Replaces the entry for the given key only if it is mapped to some value. | | `replace(key: K, oldValue: V, newValue: V): UIO[Boolean]` | Replaces the entry for the given key only if it was previously mapped to a given value. | ### Remapping Values | Method | Definition | |----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------| | `compute(key: K, remap: (K, V) => V): UIO[Option[V]]` | Attempts to compute a mapping for the given key and its current mapped value. | | `def computeIfAbsent(key: K, map: K => V): UIO[V]` | Computes a value of a non-existing key. | | `computeIfPresent(key: K, remap: (K, V) => V): UIO[Option[V]]` | Attempts to compute a new mapping of an existing key. | ## Retrieval Operations | Method | Definition | |-------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------| | `get(key: K): UIO[Option[V]]` | Retrieves the value associated with the given key. | | `exists(p: (K, V) => Boolean): UIO[Boolean]` | Tests whether a given predicate holds true for at least one element in a map. | | `collectFirst[B](pf: PartialFunction[(K, V), B]): UIO[Option[B]]` | Finds the first element of a map for which the partial function is defined and applies the function to it. | | `fold[S](zero: S)(f: (S, (K, V)) => S): UIO[S]` | Folds the elements of a map using the given binary operator. | | `forall(p: (K, V) => Boolean): UIO[Boolean]` | Tests whether a predicate is satisfied by all elements of a map. | | `isEmpty: UIO[Boolean]` | True if there are no elements in this map. | | `toChunk: UIO[Chunk[(K, V)]]` | Collects all entries into a chunk. | | `toList: UIO[List[(K, V)]]` | Collects all entries into a list. | ## Example Usage Given: ```scala for { emptyMap <- ConcurrentMap.empty[Int, String] data <- ZIO.succeed(Chunk(1 -> "A", 2 -> "B", 3 -> "C")) mapA <- ConcurrentMap.fromIterable(data) map100 <- ConcurrentMap.make(1 -> 100) mapB <- ConcurrentMap.make(("A", 1), ("B", 2), ("C", 3)) } yield () ``` | Operation | Result | |----------------------------------------------------------|---------| | `mapA.collectFirst { case (3, _) => "Three" }` | "Three" | | `mapA.collectFirst { case (4, _) => "Four" }` | Empty | | `map100.compute(1, _+_).get(1)` | 101 | | `emptyMap.computeIfAbsent("abc", _.length).get("abc")` | 3 | | `map100.computeIfPresent(1, _+_).get(1)` | 101 | | `mapA.exists((k, _) => k % 2 == 0)` | true | | `mapA.exists((k, _) => k == 4)` | false | | `mapB.fold(0) { case (acc, (_, value)) => acc + value }` | 6 | | `mapB.forall((_, v) => v < 4)` | true | | `emptyMap.get(1)` | None | | `emptyMap.put(1, "b").get(1)` | "b" | | `mapA.putIfAbsent(2, "b").get(2)` | "B" | | `emptyMap.putAll((1, "A"), (2, "B"), (3, "C")).get(1)` | "A" | | `mapA.remove(1).get(1)` | None | | `mapA.remove(1,"b").get(1)` | "A" | | `mapA.removeIf((k, _) => k != 1).get(1)` | "A" | | `mapA.removeIf((k, _) => k != 1).get(2)` | None | | `mapA.retainIf((k, _) => k == 1).get(1)` | "A" | | `mapA.retainIf((k, _) => k == 1).get(2)` | None | | `mapA.clear.isEmpty` | true | --- ## ConcurrentSet A `ConcurrentSet` is a Set wrapper over `java.util.concurrent.ConcurrentHashMap`. ## Creation | Method | Definition | |-------------------------------------------------------------|----------------------------------------------------------------------| | `empty[A]: UIO[ConcurrentSet[A]]` | Makes an empty `ConcurrentSet` | | `empty[A](initialCapacity: Int): UIO[ConcurrentSet[A]]` | Makes an empty `ConcurrentSet` with initial capacity | | `fromIterable[A](as: Iterable[(A)]): UIO[ConcurrentSet[A]]` | Makes a new `ConcurrentSet` initialized with the provided collection | | `make[A](as: A*): UIO[ConcurrentSet[A]]` | Makes a new `ConcurrentSet` initialized with the provided elements | ## Update Operations Basic operations are provided to manipulate the values in the `ConcurrentSet`: ### Adding Values | Method | Definition | |-----------------------------------------|---------------------| | `add(x: A): UIO[Boolean]` | Adds a new value. | | `addAll(xs: Iterable[A]): UIO[Boolean]` | Adds all new values.| ### Updating Values | Method | Definition | |-----------------------------------|-----------------------------------------------------------------------| | `transform(f: A => A): UIO[Unit]` | Transform all elements of the ConcurrentSet using the given function. | ### Removing Values | Method | Definition | |--------------------------------------------|-----------------------------------------------------------------------------------------| | `remove(x: A): UIO[Boolean]` | Removes the entry for the given value if it is mapped to an existing element. | | `removeAll(xs: Iterable[A]): UIO[Boolean]` | Removes all the entries for the given values if they are mapped to an existing element. | | `removeIf(p: A => Boolean): UIO[Boolean]` | Removes all elements which satisfy the given predicate. | | `retainAll(xs: Iterable[A]): UIO[Boolean]` | Retain all the entries for the given values if they are mapped to an existing element. | | `retainIf(p: A => Boolean): UIO[Boolean]` | Removes all elements which do not satisfy the given predicate. | | `clear: UIO[Unit]` | Removes all elements. | ## Retrieval Operations | Method | Definition | |---------------------------------------------------------------|------------------------------------------------------------------------------------------------------------| | `collectFirst[B](pf: PartialFunction[(A, B)): UIO[Option[B]]` | Finds the first element of a set for which the partial function is defined and applies the function to it. | | `exists(p: A => Boolean): UIO[Boolean]` | Tests whether a given predicate holds true for at least one element in the set. | | `fold[R, E, S](zero: S)(f: (S, A) => S): UIO[S]` | Folds the elements of a set using the given binary operator. | | `forall(p: A => Boolean): UIO[Boolean]` | Tests whether a predicate is satisfied by all elements of a set. | | `find[B](p: A => Boolean): UIO[Option[A]]` | Retrieves the elements in which predicate is satisfied. | | `contains(x: A): UIO[Boolean]` | Tests whether if the element is in the set. | | `containsAll(xs: Iterable[A]): UIO[Boolean]` | Tests if the elements in the collection are a subset of the set. | | `size: UIO[Int]` | Number of elements in the set. | | `isEmpty: UIO[Boolean]` | True if there are no elements in the set. | | `toSet: UIO[Set[A]]` | Convert the ConcurrentSet to Set. | ## Example Usage Given: ```scala for { emptySet <- ConcurrentSet.empty[Int] setA <- ConcurrentSet.make[Int](1, 2, 3, 4) } yield () ``` | Operation | Result | |-------------------------------------------|-----------------------| | `emptySet.add(1).toSet` | Set(1) | | `setA.addAll(Chunk(5, 6).toSet)` | Set(1, 2, 3, 4, 5, 6) | | `setA.remove(1).toSet` | Set(2, 3, 4) | | `setA.removeAll(1, 3).toSet` | Set(2, 4) | | `setA.retainAll(List(1, 3, 5, 6)).toSet` | Set(1, 3) | | `setA.clear.isEmpty` | true | | `setA.contains(5)` | false | | `setA.containsAll(Chunk(1, 2, 3))` | true | | `setA.exists(_ > 4)` | false | | `setA.forAll(_ < 5)` | true | | `setA.removeIf(_ % 2 == 0)` | Set(1, 3) | | `setA.retainIf(_ % 2 == 0)` | Set(2, 4) | | `setA.find(_ > 2)` | Set(3, 4) | | `setA.collectFirst { case 3 => "Three" }` | Set(3) | | `setA.size` | 4 | | `setA.transform(_ + 10)` | Set(11, 12, 13, 14) | | `setA.fold(0)(_ + _)` | 10 | --- ## CountdownLatch A synchronization aid that allows one or more fibers to wait until a set of operations being performed in other fibers completes. A `CountDownLatch` is initialized with a given count. The `await` method block until the current count reaches zero due to invocations of the `countDown` method, after which all waiting fibers are released and any subsequent invocations of `await` return immediately. This is a one-shot phenomenon -- the count cannot be reset. If you need a version that resets the count, consider using a [`CyclicBarrier`](cyclicbarrier.md). A `CountDownLatch` is a versatile synchronization tool and can be used for a number of purposes. A `CountDownLatch` initialized with a count of one serves as a simple on/off latch, or gate: all fibers invoking `await` wait at the gate until it is opened by a fiber invoking `countDown`. A `CountDownLatch`initialized to N can be used to make one fiber wait until N fibers have completed some action, or some action has been completed N times. A useful property of a `CountDownLatch` is that it doesn't require that fibers calling `countDown` wait for the count to reach zero before proceeding, it simply prevents any fiber from proceeding past an `await`until all fibers could pass. ## Creation To create a `CountDownLatch` we can simply use the `make` constructor. It takes an initial number, for the countdown counter: ```scala object CountdownLatch { def make(n: Int): IO[Option[Nothing], CountdownLatch] } ``` ## Operations There are two important operations defined on `CountdownLatch`: ```scala class CountdownLatch { val countDown: UIO[Unit] val await: UIO[Unit] } ``` The **`countDown`** operation decrements the count of the latch, releasing all waiting fibers if the count reaches zero, and the **`await`** operation causes the current fiber to wait until the latch has counted down to zero. ## Examples ### Simple on/off Latch We can simply create an on/off latch using `Promise`. In the following example, we don't want to start the `consume` process before the first `50` number appears in the queue. As it requires a simple on/of latch we can implement that using the `Promise` data type: ```scala object MainApp extends ZIOAppDefault { def consume(queue: Queue[Int]): UIO[Nothing] = queue.take .flatMap(i => ZIO.debug(s"consumed: $i")) .forever def produce(queue: Queue[Int], latch: Promise[Nothing, Unit]): UIO[Nothing] = (Random .nextIntBounded(100) .tap(i => queue.offer(i)) .tap(i => ZIO.when(i == 50)(latch.succeed(()))) *> ZIO.sleep(500.millis)).forever def run = for { latch <- Promise.make[Nothing, Unit] queue <- Queue.unbounded[Int] _ <- produce(queue, latch) <&> (latch.await *> consume(queue)) } yield () } ``` Alternatively, we can have an on/off latch using `CountDownLatch` with an initial count of _one_: ```scala object MainApp extends ZIOAppDefault { def consume(queue: Queue[Int]): UIO[Nothing] = queue.take .flatMap(i => ZIO.debug(s"consumed: $i")) .forever def produce(queue: Queue[Int], latch: CountdownLatch): UIO[Nothing] = (Random .nextIntBounded(100) .tap(i => queue.offer(i)) .tap(i => ZIO.when(i == 50)(latch.countDown)) *> ZIO.sleep(500.millis)).forever def run = for { latch <- CountdownLatch.make(1) queue <- Queue.unbounded[Int] _ <- produce(queue, latch) <&> (latch.await *> consume(queue)) } yield () } ``` ### Advanced Latches We can solve more advanced problems by increasing the initial count of `CountdownLatch`. Assume we had several producers concurrently in the previous example and the consumer was required to wait until at least five 50 numbers were added to the queue before they were allowed to consume. We can do this as follows: ```scala object MainApp extends ZIOAppDefault { def consume(queue: Queue[Int]): UIO[Nothing] = queue.take .flatMap(i => ZIO.debug(s"consumed: $i")) .forever def produce(queue: Queue[Int], latch: CountdownLatch): UIO[Nothing] = (Random .nextIntBounded(100) .tap(i => queue.offer(i)) .tap(i => ZIO.when(i == 50)(latch.countDown)) *> ZIO.sleep(500.millis)).forever def run = for { latch <- CountdownLatch.make(5) queue <- Queue.unbounded[Int] p = ZIO.collectAllParDiscard(ZIO.replicate(10)(produce(queue, latch))) c = latch.await *> consume(queue) _ <- p <&> c } yield () } ``` In this example, 10 producers are producing numbers concurrently, and the consumer is waiting for its condition to be fulfilled to start the consumption process. --- ## CyclicBarrier A synchronization aid that allows a set of fibers to all wait for each other to reach a common barrier point. CyclicBarriers are useful in programs involving a fixed sized party of fibers that must occasionally wait for each other. The barrier is called cyclic because it can be re-used after the waiting fibers are released. ## Creation To create a `CyclicBarrier` we must provide the number of parties, and we can also provide an optional action: 1. **number of parties**— The fibers that need to synchronize their execution are called _parties_. This number denotes how many parties must occasionally wait for each other. In other words, it specifies the number of parties required to trip the barrier. 2. **action**— An optional command that is run once per barrier point, after the last fiber in the party arrives, but before any fibers are resumed. This action is useful for updating the shared state before any of the parties continue. ```scala object CyclicBarrier { def make(parties: Int) : UIO[CyclicBarrier] = ??? def make(parties: Int, action: UIO[Any]): UIO[CyclicBarrier] = ??? } ``` If we create a barrier and don't call `await` on that, the barrier is not going to be released and the number of `waiting` fibers remains zero: ```scala for { barrier <- CyclicBarrier.make(100, ZIO.debug("This is a release action!")) isBroken <- barrier.isBroken waiting <- barrier.waiting } yield assert(!isBroken && waiting == 0) ``` ## Simple Example In the following example, we started three tasks, each one has a different working time, but they won't return until the other parties finished their jobs: ```scala object MainApp extends ZIOAppDefault { def task(name: String) = for { b <- ZIO.service[CyclicBarrier] _ <- ZIO.debug(s"task-$name: started my job right now!") d <- Random.nextLongBetween(1000, 10000) _ <- ZIO.sleep(Duration.fromMillis(d)) _ <- ZIO.debug(s"task-$name: finished my job and waiting for other parties to finish their jobs") _ <- b.await _ <- ZIO.debug(s"task-$name: the barrier is now broken, so I'm going to exit immediately!") } yield () def run = for { b <- CyclicBarrier.make(3) tasks = task("1") <&> task("2") <&> task("3") _ <- tasks.provide(ZLayer.succeed(b)) } yield () } ``` ## Cyclic Example ّIf we change the previous example and add more than three tasks, the first three arriving tasks will be blocked and wait for synchronization. After the barrier is broken, the next three tasks will be blocked on the next barrier. **This process will be executed again and again for further tasks. This is why we say that the barrier is cyclic**: ```scala object MainApp extends ZIOAppDefault { def task(name: String) = for { b <- ZIO.service[CyclicBarrier] _ <- ZIO.debug(s"task-$name: started my job right now!") d <- Random.nextLongBetween(1000, 10000) _ <- ZIO.sleep(Duration.fromMillis(d)) _ <- ZIO.debug(s"task-$name: finished my job and waiting for other parties to finish their jobs") _ <- b.await _ <- ZIO.debug(s"task-$name: the barrier is now broken, so I'm going to exit immediately!") } yield () def run = for { b <- CyclicBarrier.make( parties = 3, action = ZIO.debug( "The barrier is released right now!" + "I can do some effectful actions on release of barrier." ) ) tasks = task("1") <&> task("2") <&> task("3") <&> task("4") <&> task("5") _ <- tasks.provide(ZLayer.succeed(b)) } yield () } ``` In this example after breakage of the barrier by proceeding with `task 1`, `task 2`, and `task 3`, the `CyclicBarrier` will be reset to the initial state, so other tasks can come in and `await` on the barrier. So here, `task 4` and `task 5`, proceed with their job and finally wait for all parties to come into the barrier point, but in this example, as we didn't provide `task 6`, the remaining tasks will block the execution of the whole program, infinitely; because the number of waiting fibers are not equal to `parties`. If we add another concurrent task (e.g. `task("6")`) to our list of tasks, finally the next group of jobs that are waiting for each other will trip the barrier. ## Internals Each `CyclicBarrier` has the following internal _private_ properties, knowing them helps us to have a deep understanding of how `CyclicBarrier` works: ```scala class CyclicBarrier private ( private val _parties: Int, private val _waiting: Ref[Int], private val _lock: Ref[Promise[Unit, Unit]], private val _action: UIO[Any], private val _broken: Ref[Boolean] ) ``` Let's introduce each one: 1. `_parties`— The fibers that need to synchronize their execution are called _parties_. It is an immutable property and will be assigned when we create a `CyclicBarrier` using one of the `make` constructors of the `CyclicBarrier`. 2. `_waiting`— This is a mutable property that denotes the number of already fibers waiting for the release of the barrier. These fibers are waiting together for synchronization purpose. To access this property, we can use the `waiting` member of a `CyclicBarrier` which returns `UIO[Int]`. 3. `_lock`— This is a mutable property that contains a `Promise[Unit, Unit]`: - When a barrier is _released_, the value of this promise internally will be succeeded with a `Unit` value. - When a barrier is _broken_, the value of this promise internally will be failed with a `Unit` value. - There is no public API for changing the value of this property. 4. `_action`— When we create a `CyclicBarrier` we can provide an effectful _action_ of type `UIO[Any]` which will be executed when the barrier is released before any of the parties continue. 5. `_broken`— This is a mutable property which denotes that whether the barrier is broken or not: - The default value of `_broken` is `false`. - When one of the `_waiting` fibers is interrupted, the barrier will be broken and the value of `_broken` will be changed to `true`. - We can access this value using `isBroken` method on a `CyclicBarrier`. ## Operations Let's take a look at the operations defined on a `CyclicBarrier`, then we'll drill down to the important ones: | Method | Definition | |--------------------------|---------------------------------------------------------------------------------------------| | `parties: Int` | The number of parties required to trip this barrier. | | `waiting: UIO[Int]` | The number of parties currently waiting at the barrier. | | `await: IO[Unit, Int]` | Waits until all parties have invoked await on this barrier. Fails if the barrier is broken. | | `reset: UIO[Unit]` | Resets the barrier to its initial state. Breaks any waiting party. | | `isBroken: UIO[Boolean]` | Queries if this barrier is in a broken state. | ### reset When we reset a barrier, the barrier will be reset to its _initial state_ through the following uninterruptible steps: - It breaks any waiting party. So all _waiting_ fibers will be interrupted correspondingly. - The barrier will be ready to synchronize the next groups of parties. So further `await` calls will be accepted for synchronization. This is why we say that the barrier is cyclic. - Number of _waiting_ fibers will be reset to zero, so there is no fiber in a _waiting_ state. - If the barrier is broken, it will set its _broken status_ to `false`. Here is an example shows the mechanism of `reset` method: ```scala object MainApp extends ZIOAppDefault { def task(name: String, b: CyclicBarrier) = for { _ <- ZIO.debug(s"task-$name: started my job right now!") _ <- b.await _ <- ZIO.debug( s"task-$name: the barrier is now released, " + s"so I'm going to exit immediately!" ) } yield () def run = for { b <- CyclicBarrier.make(3) f1 <- task("1", b).fork f2 <- task("2", b).fork f3 <- (ZIO.sleep(1.second) *> task("3", b)) .onInterrupt( ZIO.debug( "task-3: I started my job with some delay! " + "so before getting the chance to await on the barrier, " + "the reset operation interrupted me!" ) ) .fork _ <- f1.status.repeatWhile(!_.isInstanceOf[Fiber.Status.Suspended]) _ <- f2.status.repeatWhile(!_.isInstanceOf[Fiber.Status.Suspended]) _ <- b.waiting.debug("waiting fibers before reset") _ <- ZIO.whenZIO(f3.status.map(_.isInstanceOf[Fiber.Status.Running]))(b.reset) _ <- b.waiting.debug("waiting fibers after reset") _ <- f1.join _ <- f2.join _ <- f3.join } yield () } ``` ### await When we call `await` on a `CyclicBarrier`, it will return a value of type `IO[Unit, Int]` through the following uninterruptible steps: - If the barrier is broken, it will fail with the type of `Unit`. - Then, it will wait until all parties have invoked `await` on this barrier: - If the number of _waiting_ fibers reaches the number of _parties_: - First, the optional _action_ effect will be performed. - Before resuming all _waiting_ fibers, the barrier will be reset to its _initial state_ using the `reset` method. - Accordingly, all parties that are in _waiting_ state due to the call to `await` method will resume and continue processing. - If the number of _waiting_ fibers is not reached the number of _parties_, it will suspend the fiber (and that fiber will become one of the _waiting_ fibers) until all parties have invoked `await` on this barrier. During this process, if any waiting fibers are interrupted, [the barrier will be broken](#barrier-breakage-model). ### Barrier Breakage Model A barrier can be broken in one of the following cases: 1. The `CyclicBarrier` uses an _all-or-none breakage model_ for failed synchronization attempts: If a fiber leaves a barrier point prematurely because of interruption, failure, or timeout, all other fibers waiting at that barrier point will break other parties. 2. Manual reset of a barrier will break all waiting parties. An example: ```scala for { barrier <- CyclicBarrier.make(100) f1 <- barrier.await.timeout(1.second).fork f2 <- barrier.await.fork _ <- f1.status.repeatWhile(!_.isInstanceOf[Fiber.Status.Suspended]) _ <- f2.status.repeatWhile(!_.isInstanceOf[Fiber.Status.Suspended]) isBroken1 <- barrier.isBroken _ <- TestClock.adjust(1.second) isBroken2 <- barrier.isBroken res1 <- f1.await res2 <- f2.await } yield assert(!isBroken1 && isBroken2) ``` --- ## Introduction to ZIO's Synchronization Primitives When we access shared resources in a concurrent environment, we should choose a proper synchronization mechanism to avoid incorrect results and data inconsistencies. ZIO provides a set of synchronization primitives and concurrent data structures in the `zio-concurrent` module that helps us to achieve the desired synchronization. ## Installation In order to use this library, we need to add the following line in our `build.sbt` file: ```scala libraryDependencies += "dev.zio" %% "zio-concurrent" % "2.x.x" ``` ## Synchronization ZIO has several synchronization tools: - **[`ReentrantLock`](reentrantlock.md)**— The `ReentrantLock` is a synchronization tool that is useful for synchronizing blocks of code. - **[`CountDownLatch`](countdownlatch.md)**— The `CountDownLatch` is a synchronization tool that allows one or more fibers to wait for the finalization of multiple operations. - **[`CyclicBarrier`](cyclicbarrier.md)**— The `CyclicBarrier` is a synchronization tool that allows a set of fibers to all wait for each other to reach a common barrier point. ## Concurrent Data Structures It also has some concurrent data structure: - **[`ConcurrentMap`](concurrentmap.md)**— A `ConcurrentMap` is a Map wrapper over `java.util.concurrent.ConcurrentHashMap` - **[`ConcurrentSet`](concurrentset.md)**— A `ConcurrentSet` is a Set wrapper over `java.util.concurrent.ConcurrentHashMap`. --- ## MVar An `MVar[A]` is a mutable location that is either empty or contains a value of type `A`. So the `MVar` acts like a _single-element buffer_. `MVar` can be used in multiple different ways: - As a simple on/off latch - As a binary semaphore `MVar[Unit]`, with `take` and `put` as `acquire` and `release` - As a synchronized mutable variable - As a channel, with `take` and `put` as `receive` and `send` They were introduced in the paper [Concurrent Haskell](http://research.microsoft.com/~simonpj/papers/concurrent-haskell.ps.gz) by Simon Peyton Jones, Andrew Gordon and, Sigbjorn Finne. ## Creation There are two ways to create an `MVar`: 1. **`MVar.empty[A]`**— To create an `MVar` of type `A` that is _initially empty_, for example: ```scala val empty = MVar.empty[Int] ``` 2. **`MVar.make[A]`**— To create an `MVar` of type `A` that is _initially full_, for example: ```scala val full = MVar.make(42) ``` ## Operations ### Blocking `put` and `take` `MVar` has two fundamental operations: - `MVar#put` which fills an `MVar` if it is empty and blocks otherwise. - `MVar#take` which empties an `MVar` if it is full and blocks otherwise. ```scala class MVar[A] { def put(a: A): UIO[Unit] = ??? def take: UIO[A] = ??? } ``` So we can put something into it, making it full, or take something out, making it empty, and in two cases, it will block the calling fiber: - If it is full and the calling fiber tries to put something in it. - If it is empty and the calling fiber tries to take something out of it. These two features of `MVar` make it possible to synchronize multiple fibers. ### Nonblocking `tryPut` and `tryTake` While `put` and `take` are blocking operations, there are also non-blocking versions of these operations: - `MVar#tryPut` which tries to fill an `MVar` and returns `true` if successful or `false` if it is full. - `MVar#tryTake` which tries to empty an `MVar` and returns `Some(x)` if it is full of `x` or `None` if it is empty. ```scala class MVar[A] { def tryPut(x: A): UIO[Boolean] = ??? def tryTake: UIO[Option[A]] = ??? } ``` ### Blocking `update`, and `modify` Using `update` and `modify` we can update the value of an `MVar`. The `update` doesn't return the updated value, but the `modify` does: ```scala class MVar[A] { def update(f: A => A): UIO[Unit] = ??? def modify[B](f: A => (B, A)): UIO[B] = ??? } ``` Like the `put` and `take` operations, the `update` and `modify` operations are blocking, this means if the `MVar` is empty, they will block the calling fiber until the `MVar` becomes full. ## Use Cases ### Simple On/Off Latch We can use an `MVar` to implement a simple on/off latch: ```scala object MainApp extends ZIOAppDefault { def job1(latch: MVar[Unit]) = for { _ <- ZIO.debug("Job 1: I started my work") _ <- ZIO.sleep(5.second) _ <- ZIO.debug("Job 1: I finished my work") _ <- latch.put(()) } yield () def job2(latch: MVar[Unit]) = for { _ <- ZIO.debug("Job 2: I'm waiting for job 1 to finish its work") _ <- latch.take _ <- ZIO.debug("Job 2: I'm starting my work") _ <- ZIO.sleep(4.second) _ <- ZIO.debug("Job 2: I finished my work") } yield () def run = MVar.empty[Unit].flatMap { latch => job1(latch) <&> job2(latch) } } ``` In the above example, we created an empty `MVar`, and then we created two `ZIO` workflows that will be executed concurrently. The first one will wait for the second one to finish its work. But the second one at some point in its execution will need to synchronize with the first one. It needs to make sure that the first one has finished its work before it continues its own work. ### Binary Semaphore Assume we have a function `inc` that takes a `Ref[Int]` and increments its value by one as below: ```scala object MainApp extends ZIOAppDefault { def inc(ref: Ref[Int]) = for { v <- ref.get result = v + 1 _ <- ref.set(result) } yield () def run = for { ref <- Ref.make(0) _ <- ZIO.foreachParDiscard(1 to 100)(_ => inc(ref)) _ <- ref.get.debug("result") } yield () } ``` When we perform the `inc` function, 100 times, we expect the final value of the `ref` to be 100. But if we run the program multiple times, we will get different results. This is because the `inc` function is not atomic, and the `ref` may be updated by another thread between the time we read it and the time we write it. So we need a way to ensure that between the time we read the ref and the time we write to it, no other threads will be able to make changes to it. We know that `Ref` has the `update` operation that is atomic. So if we rewrite the `inc` as below, our program will work as expected: ```scala def inc(ref: Ref[Int]) = ref.update(_ + 1) ``` Although the solution to this problem is `Ref#update`, we want to use `MVar` to implement the same functionality for pedagogical purposes. So let's see how we can do that using `MVar`: ```scala object MainApp extends ZIOAppDefault { def inc(ref: Ref[Int]) = for { v <- ref.get result = v + 1 _ <- ref.set(result) } yield () def run = for { semaphore <- MVar.make[Unit](()) ref <- Ref.make(0) _ <- ZIO.foreachParDiscard(1 to 100) { _ => for { _ <- semaphore.take // acquire _ <- inc(ref) _ <- semaphore.put(()) // release } yield () } _ <- ref.get.debug("result") } yield () } ``` So we used the `take` as `acquire` and the `put` as the `release` operation of the binary semaphore. Note that, in the above solution, if any interruption occurs while we have acquired the semaphore (between `acquire` and `release` operations), the semaphore will not be released. So to prevent such a situation, we need to make sure that we always release the semaphore whether the critical section runs successfully or not. Let's model the whole solution in a new data type called `BinarySemaphore`: ```scala class BinarySemaphore private (mvar: MVar[Unit]) { def acquire: ZIO[Any, Nothing, Unit] = mvar.take def release: ZIO[Any, Nothing, Unit] = mvar.put(()) def guard[R, E, A]( region: ZIO[R, E, A] ): ZIO[R, E, A] = ZIO.acquireReleaseWith(acquire)(_ => release)(_ => region) } object BinarySemaphore { def make(): ZIO[Any, Nothing, BinarySemaphore] = MVar.make(()).map(new BinarySemaphore(_)) } ``` Now we can apply the `guard` function to the `inc` function of the previous example: ```scala object MainApp extends ZIOAppDefault { def inc(ref: Ref[Int]) = for { v <- ref.get result = v + 1 _ <- ref.set(result) } yield () def run = for { semaphore <- BinarySemaphore.make() ref <- Ref.make(0) _ <- ZIO.foreachParDiscard(1 to 100) { _ => semaphore.guard(inc(ref)) } _ <- ref.get.debug("result") } yield () } ``` ### Synchronized Mutable Variable We can have synchronized mutable variables using the `MVar` data type: ```scala object MainApp extends ZIOAppDefault { def inc(state: MVar[Int]) = state.update(_ + 1) def run = MVar .make(0) .flatMap(s => ZIO.foreachParDiscard(1 to 100)(_ => inc(s)) *> s.take) .debug("result") } ``` In this case, we executed the same `inc` workflow 100 times concurrently. All the concurrent fibers access the same shared mutable variable called `state` in a synchronized way. In this case, we used the `update`, a safe operation that will atomically update the value of `MVar`. A question that may be raised is that can we compose `take` and `update` to implement the same functionality for the `inc` workflow as below? ```scala def inc(state: MVar[Int]) = state.take.flatMap(s => state.put(s + 1)) ``` Can we say this is the same as the previous `inc` function? No, because although the `take` and `put` are atomic by themselves, their composition is not. So in a real-world scenario, in a concurrent environment it is possible that in between the `take` and `put` operations, the `state` is modified by another fiber. So this is why we used the `update` operation instead, which is an atomic operation. ### Producer/Consumer Channel We can use an `MVar` to implement a producer/consumer channel: ```scala object MainApp extends ZIOAppDefault { def producer(state: MVar[Int]) = Random.nextIntBounded(100) .flatMap(state.put) .forever def consumer(state: MVar[Int]) = state.take .flatMap(i => ZIO.debug(s"$i consumed!")) .delay(1.second) .forever def run = MVar.empty[Int].flatMap { s => producer(s) <&> consumer(s) } } ``` In such a case we want to model a producer/consumer channel to make sure the producer doesn't produce any value unless the consumer is ready to consume it. So in this example, `MVar` acts as one element size channel that handles backpressure. If we add more consumers, the speed of consuming elements will be increased. Note that, by having multiple consumers, the data will not be duplicated through the consumers. If we have three consumers, each piece of data will be consumed only by one of the consumers: ```scala object MainApp extends ZIOAppDefault { def producer(state: MVar[Int]) = ZIO.foreachDiscard(1 to Int.MaxValue)(state.put) def consumer(state: MVar[Int])(name: String) = state.take .flatMap(i => ZIO.debug(s"Consumer $name: $i consumed!")) .delay(1.second) .forever def run = MVar.empty[Int].flatMap { s => producer(s) <&> consumer(s)("A") <&> consumer(s)("B") <&> consumer(s)("C") } } ``` --- ## ReentrantLock A `ReentrantLock` is a lock which can be acquired multiple times by the same fiber. When a fiber acquires (`lock`) a reentrant lock, it will become the owner of that lock. Other fibers cannot obtain the lock unless the lock owner releases (`unlock`) the lock. As the lock is reentrant, the lock owner can call the `lock` again, multiple times. ## Reentrancy In reentrancy, only the current working fiber can access a shared resource, preventing any other fibers from doing so. Reentrant locks allow their owner (the fiber that owns the lock) to re-enter them multiple times. Therefore, in reentrancy locks are acquired per-fiber instead of per-invocation. In other words, if a fiber is not reentrant, and tries to acquire a lock that it already holds, the request won’t succeed. ## Creating ReentrantLocks Using `ReentrantLocks.make` we can create a reentrant lock in the _unlocked state_: ```scala object ReentrantLock { def make(fairness: Boolean = false): UIO[ReentrantLock] = ??? } ``` By default, it creates a reentrant lock with an unfair policy, so waiters will be picked randomly. If we set the `fairness` parameter to `true`, the reentrant lock will pick the longest waiting fiber. ## Locking and Unlocking The two basic operations on reentrant locks are `lock` and `unlock`. They acquire and release the lock, respectively: ```scala trait ReentrantLock { lazy val lock: UIO[Unit] lazy val unlock: UIO[Unit] } ``` 1. **`ReentrantLock#lock`**— When a fiber attempt to acquire the lock one of the following cases will happen: - When the state is _unlocked_ and in another word if the lock is not held by another fiber, it will acquire the lock and returns immediately and the _hold count_ increased by one. - When the state is _locked_ and the current fiber already holds the lock, then the _hold count_ is incremented by one, and the method returns immediately. - When the state is _locked_ and the lock is held by another fiber, then the current fiber will be put to sleep until the lock has been acquired, at which point the lock hold count will be reset to one. 2. **`ReentrantLock#unlock`**— When a fiber attempt to release the lock, one of the following cases will happen: - If the current fiber is the holder of this lock then the hold count is decremented. If the hold count is now zero then the lock is released. So if there are any fibers blocked on acquire, one fiber will be picked using (fairness or unfairness policy) and woken up. - If the current fiber is not the holder of this lock then nothing happens. ## Fairness Policy The ReentrantLock constructor offers two fairness policies: - unfair policy (the default) - fair policy When a fiber fails to acquire the lock, it is placed in the waiting queue. So when the owning fiber releases the lock, the next waiting fiber chosen by the fairness policy is allowed to try acquiring the lock: - In the case of a fairness policy, fibers always acquire a lock in the order in which they requested it. So the reentrant lock will pick the longest waiting fiber from the waiting queue. - In case of unfair policy, the reentrant lock will pick a random fiber from the waiting queue. ## Convenience Operations 1. **`ReentrantLock#tryLock`**— Acquires the lock only if it is not held by another fiber at the time of invocation otherwise it will return immediately, so it is a non-blocking operation. - When the state is _unlocked_ `tryLock` changes the state to _locked_ (with the current fiber as owner and a hold count of 1) and returns `true`. - When the state is _locked_ `tryLock` leaves the state _unchanged_ and returns `false`. ```scala trait ReentrantLock { lazy val tryLock: UIO[Boolean] } ``` 2. **`ReentrantLock#withLock`**— Acquires and releases the lock as a scoped effect. By using this method, the unlock method will be called automatically at the end of the scope. ```scala trait ReentrantLock { lazy val withLock: URIO[Scope, Int] } ``` ## Querying ReentrantLocks A reentrant lock has two states: _locked_ or _unlocked_. When the reentrant lock is in _locked_ state it has these properties: - **Owner** indicates which fiber has acquired the lock. This can be queried by calling the `ReentrantLock#owner` method. - **Hold Count** indicates how many times its owner acquired the lock. This can be queried using by calling the `ReentrantLock#holdCount` method. - **Waiters** is a collection of fibers that are waiting to acquire this lock. We can query all of them using the `ReentrantLock#queuedFibers` method. ## Examples ### Example of Simple Locking Mechanism In the following example, the main fiber acquires the lock, and then we try to acquire the lock from its child fiber. We will see that the child fiber will be blocked when it attempts to acquire the lock until the parent fiber releases it: ```scala object MainApp extends ZIOAppDefault { def run = for { l <- ReentrantLock.make() fn <- ZIO.fiberId.map(_.threadName) _ <- l.lock _ <- ZIO.debug(s"$fn acquired the lock.") task = for { fn <- ZIO.fiberId.map(_.threadName) _ <- ZIO.debug(s"$fn attempted to acquire the lock.") _ <- l.lock _ <- ZIO.debug(s"$fn acquired the lock.") _ <- ZIO.debug(s"$fn will release the lock after 5 second.") _ <- ZIO.sleep(5.second) _ <- l.unlock _ <- ZIO.debug(s"$fn released the lock.") } yield () f <- task.fork _ <- ZIO.debug(s"$fn will release the lock after 10 second.") _ <- ZIO.sleep(10.second) _ <- (l.unlock *> ZIO.debug(s"$fn released the lock.")).uninterruptible _ <- f.join } yield () } // Output: // zio-fiber-2 acquired the lock. // zio-fiber-2 will release the lock after 10 second. // zio-fiber-7 attempted to acquire the lock. // zio-fiber-2 released the lock. // zio-fiber-7 acquired the lock. // zio-fiber-7 will release the lock after 5 second. // zio-fiber-7 released the lock. ``` Parent fiber (`zio-fiber-2`) acquires the lock and then releases it after 10 seconds. Meanwhile, the child fiber (`zio-fiber-7`) tries to acquire the lock, but it cannot. The attempt to acquire the lock in the child fiber causes the fiber to go into sleep mode. Following the release of the lock by the parent fiber, the child fiber will awaken and acquire the lock. ### Example of Reentrancy In the previous example, we used the simplest use-case of a locking mechanism that doesn't involve reentrancy. To illustrate how reentrancy works, let's look at another example: ```scala object MainApp extends ZIOAppDefault { def task(l: ReentrantLock, i: Int): ZIO[Any, Nothing, Unit] = for { fn <- ZIO.fiberId.map(_.threadName) _ <- l.lock hc <- l.holdCount _ <- ZIO.debug(s"$fn (re)entered the critical section and now the hold count is $hc") _ <- ZIO.when(i > 0)(task(l, i - 1)) _ <- l.unlock hc <- l.holdCount _ <- ZIO.debug(s"$fn exited the critical section and now the hold count is $hc") } yield () def run = for { l <- ReentrantLock.make() _ <- task(l, 2) zipPar task(l, 3) } yield () } // One possible output: // zio-fiber-8 (re)entered the critical section and now the hold count is 1 // zio-fiber-8 (re)entered the critical section and now the hold count is 2 // zio-fiber-8 (re)entered the critical section and now the hold count is 3 // zio-fiber-8 (re)entered the critical section and now the hold count is 4 // zio-fiber-8 exited the critical section and now the hold count is 3 // zio-fiber-8 exited the critical section and now the hold count is 2 // zio-fiber-8 exited the critical section and now the hold count is 1 // zio-fiber-8 exited the critical section and now the hold count is 0 // zio-fiber-7 (re)entered the critical section and now the hold count is 1 // zio-fiber-7 (re)entered the critical section and now the hold count is 2 // zio-fiber-7 (re)entered the critical section and now the hold count is 3 // zio-fiber-7 exited the critical section and now the hold count is 2 // zio-fiber-7 exited the critical section and now the hold count is 1 // zio-fiber-7 exited the critical section and now the hold count is 0 ``` In this example, inside the `task` function, we have a critical section. Also, the `task` itself is recursive and inside the critical section, it will call itself. When a fiber tries to enter the critical section and that fiber is the owner of that critical section, the `ReentrantLock` allows that fiber to reenter, and it will increment the `holdCount` by one. ### Example of Producing Deadlock When two or more fibers wait forever for a lock held by another fiber, they have reached a deadlock. So when we are working with locks, we should be careful of avoiding deadlocks. In this example, we are just trying to show a simple possible deadlock example: ```scala object MainApp extends ZIOAppDefault { def workflow1(l1: ReentrantLock, l2: ReentrantLock) = for { f <- ZIO.fiberId.map(_.threadName) _ <- l1.lock *> ZIO.debug(s"$f locked the l1") o <- l2.owner.map(_.map(_.threadName)) _ <- ZIO.debug(s"$f trying to lock the l2 while the $o is its owner") *> l2.lock *> ZIO.debug(s"$f locked the l2") _ <- l2.unlock _ <- l1.unlock } yield () def workflow2(l1: ReentrantLock, l2: ReentrantLock) = for { f <- ZIO.fiberId.map(_.threadName) _ <- l2.lock *> ZIO.debug(s"$f locked the l2") o <- l1.owner.map(_.map(_.threadName)) _ <- ZIO.debug(s"$f trying to lock the l1 while the $o is its owner") *> l1.lock *> ZIO.debug(s"$f locked the l1") _ <- l1.unlock _ <- l2.unlock } yield () def run = for { l1 <- ReentrantLock.make() l2 <- ReentrantLock.make() _ <- workflow1(l1, l2) <&> workflow2(l1, l2) } yield () } ``` In we run this program, we have a possible deadlock situation, and it might print the following messages and lock forever: ``` zio-fiber-7 locked the l1 zio-fiber-8 locked the l2 zio-fiber-7 trying to lock the l2 while the Some(zio-fiber-8) is its owner zio-fiber-8 trying to lock the l1 while the Some(zio-fiber-7) is its owner ``` When we run two workflows concurrently, it can cause a deadlock when the first workflow obtains `l1` and in the meantime, the second workflow obtains `l2`, now: - When the first workflow tries to obtain `l2` while `l2` is being obtained by `l1`. - When the second workflow tries to obtain the `l1` while the `l2` is being obtained by `l1`. Eventually, both fibers will enter a waiting state, and there will be a deadlock. --- ## Annotating Tests ## Measuring Execution Time We can annotate the execution time of each test using the `timed` test aspect: ```scala suite("a timed suite")( test("A")(Live.live(ZIO.sleep(100.millis)).map(_ => assertTrue(true))), test("B")(assertTrue(true)), test("C")(assertTrue(true)) ) @@ TestAspect.timed ``` After running the test suite, the output should be something like this: ``` + a timed suite - 178 ms (100.00%) + A - 108 ms (60.95%) + B - 34 ms (19.39%) + C - 35 ms (19.66%) Ran 3 tests in 346 ms: 3 succeeded, 0 ignored, 0 failed ``` ## Tagging ZIO Test allows us to define some arbitrary tags. By labeling tests with one or more tags, we can categorize them, and then, when running tests, we can filter tests according to their tags. Let's tag all slow tests and run them separately: ```scala object TaggedSpecsExample extends ZIOSpecDefault { def spec = suite("a suite containing tagged tests")( test("a slow test") { longRunningAssertion } @@ TestAspect.tag("slow", "math"), test("a simple test") { assertTrue(1 + 1 == 2) } @@ TestAspect.tag("math"), test("another slow test") { anotherLongRunningAssertion } @@ TestAspect.tag("slow") ) } ``` By adding the `-tags slow` argument to the command line, we will only run the slow tests: ``` sbt> testOnly TaggedSpecsExample -- -tags slow ``` The output would be: ``` [info] running (fork) TaggedSpecsExample -tags slow [info] + a suite containing tagged tests - tagged: "slow", "math" [info] + a slow test - tagged: "slow", "math" [info] + another slow test - tagged: "slow" [info] Ran 2 tests in 162 ms: 2 succeeded, 0 ignored, 0 failed [success] Total time: 1 s, completed Nov 2, 2021, 12:36:36 PM ``` Run tests tagged with `slow` for the entire project: ``` sbt> testOnly -- -tags slow ``` --- ## Before, After, and Around Test Aspects 1. We can run an effect _before_, _after_, or _around_ every test: - `TestAspect.before` - `TestAspect.after` - `TestAspect.afterFailure` - `TestAspect.afterSuccess` - `TestAspect.around` ```scala test("before and after") { for { tmp <- System.env("TEMP_DIR") } yield assertTrue(tmp.contains("/tmp/test")) } @@ TestAspect.before( TestSystem.putEnv("TEMP_DIR", s"/tmp/test") ) @@ TestAspect.after( System.env("TEMP_DIR").flatMap(deleteDir) ) ``` 2. The `TestAspect.aroundTest` takes a scoped resource and evaluates every test within the context of the scoped function. 3. There are also `TestAspect.beforeAll`, `TestAspect.afterAll`, `afterAllFailure`, `afterAllSuccess`, and `TestAspect.aroundAll` variants. 4. Using `TestAspect.aroundWith` and `TestAspect.aroundAllWith` we can evaluate every test or all test between two given effects, `before` and `after`, where the result of the `before` effect can be used in the `after` effect. --- ## Conditional Aspects When we apply a conditional aspect, it will run the spec only if the specified predicate is satisfied. - **`ifEnv`** — Only runs a test if the specified environment variable satisfies the specified assertion. - **`ifEnvSet`** — Only runs a test if the specified environment variable is set. - **`ifProp`** — Only runs a test if the specified Java property satisfies the specified assertion. - **`ifPropSet`** — Only runs a test if the specified Java property is set. ```scala test("a test that will run if the product is deployed in the testing environment") { ??? } @@ ifEnv("ENV")(_ == "testing") test("a test that will run if the java.io.tmpdir property is available") { ??? } @@ ifEnvSet("java.io.tmpdir") ``` --- ## Configuring Tests To run cases, there are some [default configuration settings](../services/test-config.md) which are used by test runner, such as _repeats_, _retries_, _samples_ and _shrinks_. We can change these settings using test aspects: ## Number of Repeats The `repeats(n: Int)` test aspect runs each test with the number of times to repeat tests to ensure they are stable set to the specified value: ```scala test("repeating a test") { ZIO.attempt("Repeating a test to ensure its stability") .debug .map(_ => assertTrue(true)) } @@ TestAspect.nonFlaky @@ TestAspect.repeats(5) ``` ## Number of Retries The `retries(n: Int)` test aspect runs each test with the number of times to retry flaky tests set to the specified value. ## Number of Samples The `samples(n: Int)` test aspect runs each test with the number of sufficient samples to check for a random variable set to the specified value. Let's change the number of default samples in the following example: ```scala test("customized number of samples") { for { ref <- Ref.make(0) _ <- check(Gen.int)(_ => assertZIO(ref.update(_ + 1))(Assertion.anything)) value <- ref.get } yield assertTrue(value == 50) } @@ TestAspect.samples(50) ``` ## Maximum Number of Shrinks The `shrinks(n: Int)` test aspect runs each test with the maximum number of shrinkings to minimize large failures set to the specified value. --- ## Debugging and Diagnostics ## Debugging The `TestConsole` service has two modes debug and silent state. ZIO Test has two corresponding test aspects to switch the debug state on and off: 1. `TestAspect.debug` — When the `TestConsole` is in the debug state, the console output is rendered to the standard output in addition to being written to the output buffer. We can manually enable this mode by using `TestAspect.debug` test aspect. 2. `TestAspect.silent` — This test aspect turns off the debug mode and turns on the silent mode. So the console output is only written to the output buffer and not rendered to the standard output. ## Diagnostics The `diagnose` is an aspect that runs each test on a separate fiber and prints a fiber dump if the test fails or has not terminated within the specified duration. --- ## Environment-specific Tests ## OS-specific Tests To run a test on a specific operating system, we can use one of the `unix`, `mac` or `windows` test aspects or a combination of them. Additionally, we can use the `os` test aspect directly: ```scala suite("os")( test("unix test") { ZIO.attempt("running on unix/linux os") .debug .map(_ => assertTrue(true)) } @@ TestAspect.unix, test("macos test") { ZIO.attempt("running on macos") .debug .map(_ => assertTrue(true)) } @@ TestAspect.os(_.isMac) ) ``` ## Platform-specific Tests Sometimes we have platform-specific tests. Instead of creating separate sources for each platform to test those tests, we can use a proper aspect to run those tests on a specific platform. To run a test on a specific platform, we can use one of the `jvm`, `js`, or `native` test aspects or a combination of them. If we want to run our test only on one of these platforms, we can use one of the `jvmOnly`, `jsOnly`, or `nativeOnly` test aspects. To exclude one of these platforms, we can use the `exceptJs`, `exceptJVM`, or `exceptNative` test aspects: ```scala test("Java virtual machine name can be accessed") { for { vm <- live(System.property("java.vm.name")) } yield assertTrue(vm.get.contains("VM")) } @@ TestAspect.jvmOnly ``` ## Version-specific Tests Various test aspects can be used to run tests for specific versions of Scala, including `scala2`, `scala212`, `scala213`, and `dotty`. As in the previous section, these test aspects have corresponding `*only` and `except*` versions. --- ## Execution Strategy ZIO Test has two different strategies to run members of a test suite: _sequential_ and _parallel_. Accordingly, there are two test aspects for specifying the execution strategy: ## Parallel The default strategy is parallel. We can explicitly enable it using `TestAspect.parallel`: ```scala suite("Parallel")( test("A")(Live.live(ZIO.attempt("Running Test A").delay(1.second)).debug.map(_ => assertTrue(true))), test("B")(ZIO.attempt("Running Test B").debug.map(_ => assertTrue(true))), test("C")(Live.live(ZIO.attempt("Running Test C").delay(500.millis)).debug.map(_ => assertTrue(true))) ) @@ TestAspect.parallel ``` After running this suite, we have the following output: ``` Running Test B Running Test C Running Test A + Parallel + A + B + C ``` To change the degree of the parallelism, we can use the `parallelN` test aspect. It takes the number of fibers and executes the members of a suite in parallel up to the specified number of concurrent fibers. ## Sequential To execute them sequentially, we can use the `sequential` test aspect: ```scala suite("Sequential")( test("A")(Live.live(ZIO.attempt("Running Test A").delay(1.second)).debug.map(_ => assertTrue(true))), test("B")(ZIO.attempt("Running Test B").debug.map(_ => assertTrue(true))), test("C")(Live.live(ZIO.attempt("Running Test C").delay(500.millis)).debug.map(_ => assertTrue(true))) ) @@ TestAspect.sequential ``` And here is the output: ``` Running Test A Running Test B Running Test C + Sequential + A + B + C ``` --- ## Flaky and Non-flaky Tests Whenever we deal with concurrency issues or race conditions, we should ensure that our tests pass consistently. The `nonFlaky` is a test aspect to do that. It will run a test several times, by default 100 times, and if all those times pass, it will pass, otherwise, it will fail: ```scala test("random value is always greater than zero") { for { random <- Random.nextIntBounded(100) } yield assertTrue(random > 0) } @@ nonFlaky ``` Additionally, there is a `TestAspect.flaky` test aspect which retries a test until it succeeds. --- ## Ignoring Tests To ignore running a test, we can use the `ignore` test aspect: ```scala test("an ignored test") { assertTrue(false) } @@ TestAspect.ignore ``` To fail all ignored tests, we can use the `success` test aspect: ```scala suite("sample tests")( test("an ignored test") { assertTrue(false) } @@ TestAspect.ignore, test("another ignored test") { assertTrue(true) } @@ TestAspect.ignore ) @@ TestAspect.success ``` --- ## Introduction to Test Aspects A `TestAspect` is an aspect that can be weaved into specs. We can think of an aspect as a polymorphic function, capable of transforming one test into another, possibly enlarging the environment or error type. We use them to change existing tests or even entire suites or specs that we have already created. We can think of a test aspect as a Spec transformer. It takes one spec, transforms it, and produces another spec (`Spec => Spec`). Test aspects are applied to a test or suite using the `@@` operator: ```scala test("a single test") { ??? } @@ testAspect suite("suite of multiple tests") { ??? } @@ testAspect ``` Test aspects encapsulate cross-cutting concerns and increase the modularity of our tests. So we can focus on the primary concerns of our tests and at the end of the day, we can apply required aspects to our tests. The great thing about test aspects is that they are very composable. So we can chain them one after another. We can even have test aspects that modify other test aspects. Let's say we have the following test: ```scala test("test") { assertTrue(true) } ``` We can pass this test to whatever test aspect we want. For example, to run this test only on the JVM and repeat it five times, we can write the test as below: ```scala repeat(Schedule.recurs(5))( jvmOnly( test("test") { assertTrue(true) } ) ) ``` To compose the aspects, we have a very nice `@@` syntax, which helps us to write tests concisely. So the previous example can be written as follows: ```scala test("test") { assertTrue(true) } @@ jvmOnly @@ repeat(Schedule.recurs(5)) ``` When composing test aspects, **the order of test aspects is important**. So if we change the order, their behavior may change. For example, the following test will repeat the test 2 times: ```scala suite("suite")( test("A") { ZIO.debug("executing test") .map(_ => assertTrue(true)) }, ) @@ nonFlaky @@ repeats(2) ``` The output: ``` executing test executing test executing test + suite - repeated: 2 + A - repeated: 2 Ran 1 test in 343 ms: 1 succeeded, 0 ignored, 0 failed ``` But the following test aspect repeats the test 100 times: ```scala suite("suite")( test("A") { ZIO.debug("executing test") .map(_ => assertTrue(true)) }, ) @@ repeats(2) @@ nonFlaky ``` The output: ``` executing test executing test executing test executing test executing test ... executing test + suite - repeated: 100 + A - repeated: 100 Ran 1 test in 478 ms: 1 succeeded, 0 ignored, 0 failed ``` ## Examples So let's say we have a challenge that we need to run a test, and we want to make sure there is no flaky on the JVM, and then we want to make sure it doesn't take more than 60 seconds: ```scala test("a test with two aspects composed together") { ??? } @@ jvm(nonFlaky) @@ timeout(60.seconds) ``` This is another example of a test suite showing the use of aspects to modify test behavior: ```scala object MySpec extends ZIOSpecDefault { def spec = suite("A Suite")( test("A passing test") { assertTrue(true) }, test("A passing test run for JVM only") { assertTrue(true) } @@ jvmOnly, // @@ jvmOnly only runs tests on the JVM test("A passing test run for JS only") { assertTrue(true) } @@ jsOnly, // @@ jsOnly only runs tests on Scala.js test("A passing test with a timeout") { assertTrue(true) } @@ timeout(10.nanos), // @@ timeout will fail a test that doesn't pass within the specified time test("A failing test... that passes") { assertTrue(true) } @@ failing, //@@ failing turns a failing test into a passing test test("A ignored test") { assertTrue(false) } @@ ignore, //@@ ignore marks test as ignored test("A test using a live service instead of the test service") { for { _ <- TestClock.timeZone } yield assertCompletes } @@ withLiveClock, //@@ withLiveClock uses the live Clock service from the ZIO runtime in the test test("A flaky test that only works on the JVM and sometimes fails; let's compose some aspects!") { assertTrue(false) } @@ jvmOnly // only run on the JVM @@ eventually // @@ eventually retries a test indefinitely until it succeeds @@ timeout(20.nanos) // it's a good idea to compose `eventually` with `timeout`, or the test may never end ) @@ timeout(60.seconds) // apply a timeout to the whole suite } ``` --- ## Non-deterministic Test Data The random process of the `TestRandom` is said to be deterministic since, with the initial seed, we can generate a sequence of predictable numbers. So with the same initial seed, it will generate the same sequence of numbers. By default, the initial seed of the `TestRandom` is fixed. So repeating a generator more and more results in the same sequence: ```scala test("pseudo-random number generator with fixed initial seed") { check(Gen.int(0, 100)) { n => ZIO.attempt(n).debug.map(_ => assertTrue(true)) } } @@ samples(5) @@ after(Console.printLine("----").orDie) @@ repeat(Schedule.recurs(1)) ``` Regardless of how many times we repeat this test, the output would be the same: ``` 99 51 81 48 51 ---- 99 51 81 48 51 ---- + pseudo-random numbers with fixed initial seed - repeated: 2 Ran 1 test in 522 ms: 1 succeeded, 0 ignored, 0 failed ``` The `nondeterministic` test aspect, will change the seed of the pseudo-random generator before each test repetition: ```scala test("pseudo-random number generator with random initial seed on each repetition") { check(Gen.int(0, 100)) { n => ZIO.attempt(n).debug.map(_ => assertTrue(true)) } } @@ nondeterministic @@ samples(5) @@ after(Console.printLine("----").orDie) @@ repeat(Schedule.recurs(1)) ``` Here is a sample output, which we have different sequences of numbers on each run: ``` 73 9 17 33 10 ---- 42 85 38 2 73 ---- + pseudo-random number generator with random initial seed on each repetition - repeated: 2 Ran 1 test in 733 ms: 1 succeeded, 0 ignored, 0 failed ``` --- ## Passing Failed Tests The `failing` aspect makes a test that failed for any reason pass. ```scala test("passing a failing test") { assertTrue(false) } @@ TestAspect.failing ``` If the test passes this aspect will make it fail: ```scala test("failing a passing test") { assertTrue(true) } @@ TestAspect.failing ``` It is also possible to pass a failing test on a specified failure: ```scala test("a test that will only pass on a specified failure") { ZIO.fail("Boom!").map(_ => assertTrue(true)) } @@ TestAspect.failing[String] { case TestFailure.Assertion(_, _) => true case TestFailure.Runtime(cause: Cause[String], _) => cause match { case Cause.Fail(value, _) if value == "Boom!" => true case _ => false } } ``` --- ## Repeat and Retry There are some situations where we need to repeat a test with a specific schedule, or our tests might fail, and we need to retry them until we make sure that our tests pass. ZIO Test has the following test aspects for these scenarios: ## Repeat The `repeat` test aspect takes a `schedule` and repeats a test based on it. The test passes if it passes every time: ```scala test("repeating a test based on the scheduler to ensure it passes every time") { ZIO.debug("repeating successful tests") .map(_ => assertTrue(true)) } @@ TestAspect.repeat(Schedule.recurs(5)) ``` ## Retry If our test fails occasionally, we can retry failed tests by providing a `schedule` to the `retry` test aspect: For example, the following test retries a maximum of five times. Once a successful assertion is made, the test passes: ```scala test("retrying a failing test based on the schedule until it succeeds") { ZIO.debug("retrying a failing test") .map(_ => assertTrue(true)) } @@ TestAspect.retry(Schedule.recurs(5)) ``` ## Eventually 3. The `eventually` test aspect keeps retrying a test until it passes, regardless of how many times it fails: ```scala test("retrying a failing test until it succeeds") { ZIO.debug("retrying a failing test") .map(_ => assertTrue(true)) } @@ TestAspect.eventually ``` --- ## Restoring State of Test Services ZIO Test has some test aspects which restore the state of given restorable test services, such as `TestClock`, `TestConsole`, `TestRandom` and `TestSystem`, to their starting state after the test is run. Note that these test aspects are only useful when we are repeating tests. Here is a list of restore methods: - `TestAspect.restore` - `TestAspect.restoreTestClock` - `TestAspect.restoreTestConsole` - `TestAspect.restoreTestRandom` - `TestAspect.restoreTestSystem` - `TestAspect.restoreTestEnvironment` Let's try an example. Assume we have written the following test aspect, which repeats the test 5 times: When we run a test with this testing aspect, on each try, we have a polluted test environment: ```scala suite("clock suite")( test("adjusting clock") { for { clock <- ZIO.clock _ <- TestClock.adjust(1.second) time <- clock.currentTime(TimeUnit.SECONDS).debug("current time") } yield assertTrue(time == 1) } @@ repeat5 ) ``` This test fails in the second retry: ``` current time: 1 current time: 2 - some suite - clock suite - adjusting clock ✗ 2 was not equal to 1 time == 1 time = 2 ``` It failed because of the first run of the test changed the state of the `TestClock` service, so on the next run, the initial state of the test is not zero. In such a situation, when we are repeating a test, after each run we can restore the state of the test to its initial state, using `TestAspect.restore*` test aspects: ```scala suite("clock suite")( test("adjusting clock") { for { clock <- ZIO.clock _ <- TestClock.adjust(1.second) time <- clock.currentTime(TimeUnit.SECONDS).debug("current time") } yield assertTrue(time == 1) } @@ TestAspect.restoreTestClock @@ repeat5 ) ``` The output of running this test would be as follows: ``` current time: 1 current time: 1 current time: 1 current time: 1 current time: 1 current time: 1 + clock suite + adjusting clock Ran 1 test in 470 ms: 1 succeeded, 0 ignored, 0 failed ``` --- ## Changing the Size of Sized Generators To change the default _size_ used by [sized generators](../property-testing/built-in-generators.md#sized-generators) we can use `size` test aspect: ```scala test("generating small list of characters") { check(Gen.small(Gen.listOfN(_)(Gen.alphaNumericChar))) { n => ZIO.attempt(n).debug *> Sized.size.map(s => assertTrue(s == 50)) } } @@ TestAspect.size(50) @@ TestAspect.samples(5) ``` Sample output: ``` List(p, M) List() List(0, m, 5) List(Y) List(O, b, B, V) + generating small list of characters Ran 1 test in 676 ms: 1 succeeded, 0 ignored, 0 failed ``` --- ## Timing-out Tests The `timeout` test aspect takes a duration and times out each test. If the test case runs longer than the time specified, it is immediately canceled and reported as a failure, with a message showing that the timeout was exceeded: ```scala test("effects can be safely interrupted") { for { _ <- ZIO.attempt(println("Still going ...")).forever } yield assertTrue(true) } @@ TestAspect.timeout(1.second) ``` By applying a `timeout(1.second)` test aspect, this will work with ZIO's interruption mechanism. So when we run this test, you can see a tone of print lines, and after a second, the `timeout` aspect will interrupt that. --- ## Built-in Assertions To create `Assertion[A]` object one can use functions defined under `zio.test.Assertion`. There are already a number of useful assertions predefined like `equalTo`, `isFalse`, `isTrue`, `contains`, `throws` and more. Using the `Assertion` type effectively often involves finding the best fitting function for the type of assumptions you would like to verify. This list is intended to break up the available functions into groups based on the _Result type_. The types of the functions are included as well, to guide intuition. For instance, if we wanted to assert that the fourth element of a `Vector[Int]` was a value equal to the number `5`, we would first look at assertions that operate on `Seq[A]`, with the type `Assertion[Seq[A]]`. For this example, I would select `hasAt`, as it accepts both the position into a sequence, as well as an `Assertion[A]` to apply at that position: ```scala Assertion.hasAt[A](pos: Int)(assertion: Assertion[A]): Assertion[Seq[A]] ``` I could start by writing: ```scala val xs = Vector(0, 1, 2, 3) test("Fourth value is equal to 5") { assert(xs)(hasAt(3)(???)) } ``` The second parameter to `hasAt` is an `Assertion[A]` that applies to the third element of that sequence, so I would look for functions that operate on `A`, of the return type `Assertion[A]`. I could select `equalTo`, as it accepts an `A` as a parameter, allowing me to supply `5`: ```scala val xs = Vector(0, 1, 2, 3) test("Fourth value is equal to 5") { assert(xs)(hasAt(3)(equalTo(5))) } ``` Let's say this is too restrictive, and I would prefer to assert that a value is _near_ the number five, with a tolerance of two. This requires a little more knowledge of the type `A`, so I'll look for an assertion in the `Numeric` section. `approximatelyEquals` looks like what we want, as it permits the starting value `reference`, as well as a `tolerance`, for any `A` that is `Numeric`: ```scala Assertion.approximatelyEquals[A: Numeric](reference: A, tolerance: A): Assertion[A] ``` Changing out `equalTo` with `approximatelyEquals` leaves us with: ```scala val xs = Vector(0, 1, 2, 3) test("Fourth value is approximately equal to 5") { assert(xs)(hasAt(3)(approximatelyEquals(5, 2))) } ``` ## Any Assertions that apply to `Any` value. | Function | Result type | Description | | -------- | ----------- | ----------- | | `anything` | `Assertion[Any]` | Makes a new assertion that always succeeds. | | `isNull` | `Assertion[Any]` | Makes a new assertion that requires a null value. | | `isSubtype[A](assertion: Assertion[A])(implicit C: ClassTag[A])` | `Assertion[Any]` | Makes a new assertion that requires a value have the specified type. | | `nothing` | `Assertion[Any]` | Makes a new assertion that always fails. | | `throwsA[E: ClassTag]` | `Assertion[Any]` | Makes a new assertion that requires the expression to throw. | ## A Assertions that apply to specific values. | Function | Result type | Description | | -------- | ----------- | ----------- | | `equalTo[A](expected: A)` | `Assertion[A]` | Makes a new assertion that requires a value equal the specified value. | | `hasField[A, B](name: String, proj: A => B, assertion: Assertion[B])` | `Assertion[A]` | Makes a new assertion that focuses in on a field in a case class. | | `isOneOf[A](values: Iterable[A])` | `Assertion[A]` | Makes a new assertion that requires a value to be equal to one of the specified values. | | `not[A](assertion: Assertion[A])` | `Assertion[A]` | Makes a new assertion that negates the specified assertion. | | `throws[A](assertion: Assertion[Throwable])` | `Assertion[A]` | Makes a new assertion that requires the expression to throw. | ## Numeric Assertions on `Numeric` types | Function | Result type | Description | | -------- | ----------- | ----------- | | `approximatelyEquals[A: Numeric](reference: A, tolerance: A)` | `Assertion[A]` | Makes a new assertion that requires a given numeric value to match a value with some tolerance. | | `isNegative[A](implicit num: Numeric[A])` | `Assertion[A]` | Makes a new assertion that requires a numeric value is negative. | | `isPositive[A](implicit num: Numeric[A])` | `Assertion[A]` | Makes a new assertion that requires a numeric value is positive. | | `isZero[A](implicit num: Numeric[A])` | `Assertion[A]` | Makes a new assertion that requires a numeric value is zero. | | `nonNegative[A](implicit num: Numeric[A])` | `Assertion[A]` | Makes a new assertion that requires a numeric value is non negative. | | `nonPositive[A](implicit num: Numeric[A])` | `Assertion[A]` | Makes a new assertion that requires a numeric value is non positive. | ## Ordering Assertions on types that support `Ordering` | Function | Result type | Description | | -------- | ----------- | ----------- | | `isGreaterThan[A](reference: A)(implicit ord: Ordering[A])` | `Assertion[A]` | Makes a new assertion that requires the value be greater than the specified reference value. | | `isGreaterThanEqualTo[A](reference: A)(implicit ord: Ordering[A])` | `Assertion[A]` | Makes a new assertion that requires the value be greater than or equal to the specified reference value. | | `isLessThan[A](reference: A)(implicit ord: Ordering[A])` | `Assertion[A]` | Makes a new assertion that requires the value be less than the specified reference value. | | `isLessThanEqualTo[A](reference: A)(implicit ord: Ordering[A])` | `Assertion[A]` | Makes a new assertion that requires the value be less than or equal to the specified reference value. | | `isWithin[A](min: A, max: A)(implicit ord: Ordering[A])` | `Assertion[A]` | Makes a new assertion that requires a value to fall within a specified min and max (inclusive). | ## Iterable Assertions on types that extend `Iterable`, like `List`, `Seq`, `Set`, `Map`, and many others. | Function | Result type | Description | | -------- | ----------- | ----------- | | `contains[A](element: A)` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable contain the specified element. See Assertion.exists if you want to require an Iterable to contain an element satisfying an assertion. | | `exists[A](assertion: Assertion[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable contain an element satisfying the given assertion. | | `forall[A](assertion: Assertion[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable contain only elements satisfying the given assertion. | | `hasFirst[A](assertion: Assertion[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable to contain the first element satisfying the given assertion. | | `hasIntersection[A](other: Iterable[A])(assertion: Assertion[Iterable[A]])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires the intersection of two Iterables satisfy the given assertion. | | `hasLast[A](assertion: Assertion[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable to contain the last element satisfying the given assertion. | | `hasSize[A](assertion: Assertion[Int])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires the size of an Iterable be satisfied by the specified assertion. | | `hasAtLeastOneOf[A](other: Iterable[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable contain at least one of the specified elements. | | `hasAtMostOneOf[A](other: Iterable[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable contain at most one of the specified elements. | | `hasNoneOf[A](other: Iterable[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable contain none of the specified elements. | | `hasOneOf[A](other: Iterable[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable contain exactly one of the specified elements. | | `hasSameElements[A](other: Iterable[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable to have the same elements as the specified Iterable, though not necessarily in the same order. | | `hasSameElementsDistinct[A](other: Iterable[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable to have the same distinct elements as the other Iterable, though not necessarily in the same order. | | `hasSubset[A](other: Iterable[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires the specified Iterable to be a subset of the other Iterable. | | `isDistinct` | `Assertion[Iterable[Any]]` | Makes a new assertion that requires an Iterable is distinct. | | `isEmpty` | `Assertion[Iterable[Any]]` | Makes a new assertion that requires an Iterable to be empty. | | `isNonEmpty` | `Assertion[Iterable[Any]]` | Makes a new assertion that requires an Iterable to be non empty. | ## Ordering Assertions that apply to ordered `Iterable`s | Function | Result type | Description | | -------- | ----------- | ----------- | | `isSorted[A](implicit ord: Ordering[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable is sorted. | | `isSortedReverse[A](implicit ord: Ordering[A])` | `Assertion[Iterable[A]]` | Makes a new assertion that requires an Iterable is sorted in reverse order. | ## Seq Assertions that operate on sequences (`List`, `Vector`, `Map`, and many others) | Function | Result type | Description | | -------- | ----------- | ----------- | | `endsWith[A](suffix: Seq[A])` | `Assertion[Seq[A]]` | Makes a new assertion that requires a given string to end with the specified suffix. | | `hasAt[A](pos: Int)(assertion: Assertion[A])` | `Assertion[Seq[A]]` | Makes a new assertion that requires a sequence to contain an element satisfying the given assertion on the given position. | | `startsWith[A](prefix: Seq[A])` | `Assertion[Seq[A]]` | Makes a new assertion that requires a given sequence to start with the specified prefix. | ## Either Assertions for `Either` values. | Function | Result type | Description | | -------- | ----------- | ----------- | | `isLeft[A](assertion: Assertion[A])` | `Assertion[Either[A, Any]]` | Makes a new assertion that requires a Left value satisfying a specified assertion. | | `isLeft` | `Assertion[Either[Any, Any]]` | Makes a new assertion that requires an Either is Left. | | `isRight[A](assertion: Assertion[A])` | `Assertion[Either[Any, A]]` | Makes a new assertion that requires a Right value satisfying a specified assertion. | | `isRight` | `Assertion[Either[Any, Any]]` | Makes a new assertion that requires an Either is Right. | ## Exit/Cause/Throwable Assertions for `Exit` or `Cause` results. | Function | Result type | Description | | -------- | ----------- | ----------- | | `containsCause[E](cause: Cause[E])` | `Assertion[Cause[E]]` | Makes a new assertion that requires a Cause contain the specified cause. | | `dies(assertion: Assertion[Throwable])` | `Assertion[Exit[Any, Any]]` | Makes a new assertion that requires an exit value to die. | | `failsCause[E](assertion: Assertion[Cause[E]])` | `Assertion[Exit[E, Any]]` | Makes a new assertion that requires an exit value to fail with a cause that meets the specified assertion. | | `fails[E](assertion: Assertion[E])` | `Assertion[Exit[E, Any]]` | Makes a new assertion that requires an exit value to fail. | | `isInterrupted` | `Assertion[Exit[Any, Any]]` | Makes a new assertion that requires an exit value to be interrupted. | | `succeeds[A](assertion: Assertion[A])` | `Assertion[Exit[Any, A]]` | Makes a new assertion that requires an exit value to succeed. | | `hasMessage(message: Assertion[String])` | `Assertion[Throwable]` | Makes a new assertion that requires an exception to have a certain message. | | `hasThrowableCause(cause: Assertion[Throwable])` | `Assertion[Throwable]` | Makes a new assertion that requires an exception to have a certain cause. | ## Try | Function | Result type | Description | | -------- | ----------- | ----------- | | `isFailure(assertion: Assertion[Throwable])` | `Assertion[Try[Any]]` | Makes a new assertion that requires a Failure value satisfying the specified assertion. | | `isFailure` | `Assertion[Try[Any]]` | Makes a new assertion that requires a Try value is Failure. | | `isSuccess[A](assertion: Assertion[A])` | `Assertion[Try[A]]` | Makes a new assertion that requires a Success value satisfying the specified assertion. | | `isSuccess` | `Assertion[Try[Any]]` | Makes a new assertion that requires a Try value is Success. | ## Sum type An assertion that applies to some type, giving a method to transform the source type into another type, then assert a property on that projected type. | Function | Result type | Description | | -------- | ----------- | ----------- | | `isCase[Sum, Proj]( termName: String, term: Sum => Option[Proj], assertion: Assertion[Proj])` | `Assertion[Sum]` | Makes a new assertion that requires the sum type be a specified term. | ## Map Assertions for `Map[K, V]` | Function | Result type | Description | | -------- | ----------- | ----------- | | `hasKey[K, V](key: K)` | `Assertion[Map[K, V]]` | Makes a new assertion that requires a Map to have the specified key. | | `hasKey[K, V](key: K, assertion: Assertion[V])` | `Assertion[Map[K, V]]` | Makes a new assertion that requires a Map to have the specified key with value satisfying the specified assertion. | | `hasKeys[K, V](assertion: Assertion[Iterable[K]])` | `Assertion[Map[K, V]]` | Makes a new assertion that requires a Map have keys satisfying the specified assertion. | | `hasValues[K, V](assertion: Assertion[Iterable[V]])` | `Assertion[Map[K, V]]` | Makes a new assertion that requires a Map have values satisfying the specified assertion. | ## String Assertions for Strings | Function | Result type | Description | | -------- | ----------- | ----------- | | `containsString(element: String)` | `Assertion[String]` | Makes a new assertion that requires a substring to be present. | | `endsWithString(suffix: String)` | `Assertion[String]` | Makes a new assertion that requires a given string to end with the specified suffix. | | `equalsIgnoreCase(other: String)` | `Assertion[String]` | Makes a new assertion that requires a given string to equal another ignoring case. | | `hasSizeString(assertion: Assertion[Int])` | `Assertion[String]` | Makes a new assertion that requires the size of a string be satisfied by the specified assertion. | | `isEmptyString` | `Assertion[String]` | Makes a new assertion that requires a given string to be empty. | | `isNonEmptyString` | `Assertion[String]` | Makes a new assertion that requires a given string to be non empty. | | `matchesRegex(regex: String)` | `Assertion[String]` | Makes a new assertion that requires a given string to match the specified regular expression. | | `startsWithString(prefix: String)` | `Assertion[String]` | Makes a new assertion that requires a given string to start with a specified prefix. | ## Boolean Assertions for Booleans | Function | Result type | Description | | -------- | ----------- | ----------- | | `isFalse` | `Assertion[Boolean]` | Makes a new assertion that requires a value be false. | | `isTrue` | `Assertion[Boolean]` | Makes a new assertion that requires a value be true. | ## Option Assertions for Optional values | Function | Result type | Description | | -------- | ----------- | ----------- | | `isNone` | `Assertion[Option[Any]]` | Makes a new assertion that requires a None value. | | `isSome[A](assertion: Assertion[A])` | `Assertion[Option[A]]` | Makes a new assertion that requires a Some value satisfying the specified assertion. | | `isSome` | `Assertion[Option[Any]]` | Makes a new assertion that requires an Option is Some. | ## Unit Assertion for Unit | Function | Result type | Description | | -------- | ----------- | ----------- | | `isUnit` | `Assertion[Unit]` | Makes a new assertion that requires the value be unit. | --- ## Classic Assertions :::note In almost all cases we encourage developers using _[smart assertions](smart-assertions.md)_ instead of [classic assertions](classic-assertions.md). They are more expressive and easier to use. So you can skip reading this section. Only use _classic assertions_ when you know what you are doing. There are some rare cases where the smart assertions are not enough. ::: The `assert` and its effectful counterpart `assertZIO` are the old way of asserting ordinary values and ZIO effects. ## Asserting Ordinary Values In order to test ordinary values, we should use `assert`, like the example below: ```scala test("sum") { assert(1 + 1)(Assertion.equalTo(2)) } ``` ## Asserting ZIO Effects If we are testing an effect, we should use the `assertZIO` function: ```scala test("updating ref") { val value = for { r <- Ref.make(0) _ <- r.update(_ + 1) v <- r.get } yield v assertZIO(value)(Assertion.equalTo(1)) } ``` ## The for-comprehension Style Having this all in mind, probably the most common and also most readable way of structuring tests is to pass a for-comprehension to `test` function and yield a call to `assert` function. ```scala test("updating ref") { for { r <- Ref.make(0) _ <- r.update(_ + 1) v <- r.get } yield assert(v)(Assertion.equalTo(v)) } ``` ## Understanding the `test` Function :::note In this section we are going to learn about the internals of the `Assertion` data type. So feel free to skip this section if you are not interested. ::: In order to understand the `Assertion` data type, let's first look at the `test` function: ```scala def test[In](label: String)(assertion: => In)(implicit testConstructor: TestConstructor[Nothing, In]): testConstructor.Out ``` Its signature is a bit complicated and uses _path-dependent types_, but it doesn't matter. We can think of a `test` as a function from `TestResult` (or its effectful versions such as `ZIO[R, E, TestResult]` or `ZSTM[R, E, TestResult]`) to the `Spec[R, E]` data type: ```scala def test(label: String)(assertion: => TestResult): Spec[Any, Nothing] def test(label: String)(assertion: => ZIO[R, E, TestResult]): Spec[R, E] ``` Therefore, the function `test` needs a `TestResult`. The most common way to produce a `TestResult` is to resort to `assert` or its effectful counterpart `assertZIO`. The former one is for creating ordinary `TestResult` values and the latter one is for producing effectful `TestResult` values. Both of them accept a value of type `A` (effectful version wrapped in a `ZIO`) and an `Assertion[A]`. ## Understanding the `assert` Function Let's look at the `assert` function: ```scala def assert[A](expr: => A)(assertion: Assertion[A]): TestResult ``` It takes an expression of type `A` and an `Assertion[A]` and returns the `TestResult` which is the boolean algebra of the `AssertionResult`. Furthermore, we have an `Assertion[A]` which is capable of producing _assertion results_ on any value of type `A`. So the `assert` function can apply the expression to the assertion and produce the `TestResult`. ## Type-checker Macro To check if the code compiles, we can use the `typeCheck` macro. It is useful when we want to test if the code compiles without running it. Here is an example of how to use it: ```scala test("lazy list") { assertZIO(typeCheck( """ |val lazyList: LazyList[Int] = LazyList(1, 2, 3, 4, 5) |lazyList.foreach(println) |""".stripMargin))(isRight) } @@ TestAspect.exceptScala212 ``` The `LazyCheck` introduced in Scala 2.13, so we excluded this test from Scala 2.12. ## Examples ### Example 1: Equality Assertion Assume we have a function that concatenates two strings. One simple property of this function would be "the sum of the length of all inputs should be equal to the length of the output". Let's see an example of how we can make an assertion about this property: ```scala test("The sum of the lengths of both inputs must equal the length of the output") { check(Gen.string, Gen.string) { (a, b) => assert((a + b).length)(Assertion.equalTo(a.length + b.length)) } } ``` The syntax of assertion in the above code, is `assert(expression)(assertion)`. The first section is an expression of type `A` which is _result_ of our computation and the second one is the expected assertion of type `Assertion[A]`. ### Example 2: Field-level Assertion There is also an easy way to test an object's data for certain assertions with `hasField` which accepts besides a name, a mapping function from object to its tested property, and `Assertion` object which will validate this property. Here our test checks if a person has at least 18 years and is not from the USA. ```scala final case class Address(country:String, city:String) final case class User(name:String, age:Int, address: Address) test("Rich checking") { assert( User("Jonny", 26, Address("Denmark", "Copenhagen")) )( hasField("age", (u:User) => u.age, isGreaterThanEqualTo(18)) && hasField("country", (u:User) => u.address.country, not(equalTo("USA"))) ) } ``` What is nice about those tests is that test reporters will tell you exactly which assertion was broken. Let's say we would change `isGreaterThanEqualTo(18)` to `isGreaterThanEqualTo(40)` which will fail. Print out on the console will be a nice detailed text explaining what exactly went wrong: ```bash [info] User(Jonny,26,Address(Denmark,Copenhagen)) did not satisfy (hasField("age", _.age, isGreaterThanEqualTo(45)) && hasField("country", _.country, not(equalTo(USA)))) [info] 26 did not satisfy isGreaterThanEqualTo(45) ``` ### Example 3: Test if a ZIO Effect Fails With a Particular Error Type The following example shows how to test if a ZIO effect fails with a particular error type. To test if a ZIO effect fails with a particular error type, we can use the `ZIO#exit` to determine the exit type of that effect. ```scala case class MyError(msg: String) extends Exception val effect: ZIO[Any, MyError, Unit] = ZIO.fail(MyError("my error msg")) test("test if a ZIO effect fails with a particular error type") { for { exit <- effect.exit } yield assertTrue(exit == Exit.fail(MyError("my error msg"))) } ``` The exit method on a ZIO effect returns an `Exit` value, which represents the outcome of the effect. The `Exit` value can be either `Exit.succeed` or `Exit.fail`. If the effect succeeded, the `Exit.succeed` value will contain the result of the effect. If the effect failed, the `Exit.fail` value will contain the error that caused the failure. ### Example 4: Test if a ZIO Effect Fails With a Subtype of a Particular Error Type To test if a ZIO effect fails with a `subtype` of a particular error type, we can use the `assertZIO` function and the two `fails`, and `isSubtype` assertions from the zio-test library. The `assertZIO` function takes a ZIO effect and an assertion. The assertion is called with the result of the ZIO effect. If the assertion returns true, then the `assertZIO` will succeed, otherwise it will fail. Assume we have these error types: ```scala sealed trait MyError extends Exception case class E1(msg: String) extends MyError case class E2(msg: String) extends MyError ``` To assert if an error type is a subtype of a particular error type, we need to combine the `fails` and `isSubtype` assertions together: ```scala Assertion.fails(isSubtype[MyError](anything)) ``` Now let's look at an example: ```scala val effect = ZIO.fail(E1("my error msg")) test("Test if a ZIO effect fails with a MyError") { assertZIO(effect.exit)(fails(isSubtype[MyError](anything))) } ``` --- ## Introduction to ZIO Test Assertions Assertions are used to make sure that the assumptions on computations are exactly what we expect them to be. They are _executable checks_ for a property that must be true in our code. Also, they can be seen as a _specification of a program_ and facilitate understanding of programs. An `Assertion[A]` is a statement that can be used to assert the predicate of type `A => Boolean`. It is a piece of code that checks whether a value of type `A` satisfies some condition. If the condition is satisfied, the assertion passes; otherwise, it fails. We can think of the `Assertion[A]` as a function from `A` to `Boolean`: ```scala case class Assertion[-A](arrow: TestArrow[A, Boolean]) { def test(value: A): Boolean = ??? def run(value: => A): TestResult = ??? } ``` `Assertion` has a companion object with lots of predefined assertions that can be used to test values of different types. For example, the `Assertion.equalTo` takes a value of type `A` and returns an assertion that checks whether the value is equal to the given value: ```scala def sut = 40 + 2 val assertion: Assertion[Int] = Assertion.equalTo[Int, Int](42) assertion.test(sut) // true ``` :::note Behind the scenes, the `Assertion` type uses a `TestArrow` type to represent the function from `A` to `Boolean`. For example, instead of using a predefined `equalTo` assertion, we can create our assertion directly from a `TestArrow`: ```scala def sut = 40 + 2 val assertion: Assertion[Int] = Assertion(TestArrow.fromFunction(_ == 42)) assertion.test(sut) // true ``` Please note that the `TestArrow` is the fundamental building block of assertions specially the complex ones. Usually, as the end user, we do not require interacting with `TestArrow` directly. But it is good to know that it is there and how it works. We will see more about `TestArrow` in the next sections. ::: ## Built-in Assertions The companion object of `Assertion` provides a comprehensive set of predefined assertions that can be used to test values of different types. We have a separate page for introducing the [built-in assertions](built-in-assertions.md) in ZIO Test. ## Logical Operations As a proposition, assertions compose using logical conjunction and disjunction and can be negated: ```scala val greaterThanZero: Assertion[Int] = Assertion.isPositive val lessThanFive : Assertion[Int] = Assertion.isLessThan(5) val equalTo10 : Assertion[Int] = Assertion.equalTo(10) val assertion: Assertion[Int] = greaterThanZero && lessThanFive || !equalTo10 ``` After composing them, we can run it on any expression: ```scala val result: TestResult = assertion.run(10) ``` ## Composable Nested Assertions Besides the logical operators, we can also combine assertions like the following to have assertions on more complex types like `Option[Int]`: ```scala val assertion: Assertion[Option[Int]] = Assertion.isSome(Assertion.equalTo(5)) test("optional value is some(5)") { assert(Some(1 + 4))(assertion) } ``` This nested assertion will pass only if the given value is `Some(5)`. We can also combine assertions on more complex types like `Either[Int, Option[Int]]`: ```scala test("either value is right(Some(5))") { assert(Right(Some(1 + 4)))(isRight(isSome(equalTo(5)))) } ``` Here we're checking deeply nested values inside an `Either` and `Option`. Because `Assertion`s compose this is not a problem. All layers are being peeled off tested for the condition until the final value is reached. Here the expression `Right(Some(1 + 4))` is of type `Either[Any, Option[Int]]` and our assertion `isRight(isSome(equalTo(5)))` is of type `Assertion[Either[Any, Option[Int]]]` :::note Under the hood, the above assertion uses the `>>>` operator of `TestArrow` to make the composition of two assertions sequentially: ```scala def isRight[A]: TestArrow[Either[Any, A], A] = TestArrow.fromFunction(_.toOption.get) def isSome[A]: TestArrow[Option[A], A] = TestArrow.fromFunction(_.get) def equalTo[A, B](expected: B): TestArrow[B, Boolean] = TestArrow.fromFunction((actual: B) => actual == expected) val assertion: Assertion[Either[Any, Option[Int]]] = { val arrow: TestArrow[Either[Any, Option[Int]], Boolean] = isRight >>> // Either[Any, Option[Int]] => Option[Int] isSome[Int] >>> // Option[Int] => Int equalTo(5) // Int => Boolean Assertion(arrow) } ``` By composing an arrow of `TestArrow[Either[Any, Option[Int]], Option[Int]]` and `TestArrow[Option[Int], Boolean]` and `TestArrow[Int, Boolean]` we can create an arrow of `TestArrow[Either[Any, Option[Int]], Boolean]`. Using this technique, we can compose more arrows to create more and more complex assertions. We can see that `TestArrow` has the same analogy as [`ZLayer`](../../contextual/zlayer.md). We are dealing with generalization of functions and composition of functions in a pure and declarative fashion, which is called "arrow" in functional programming. In other words, with `TestArrow`, we have reified the concept of a function and its composition, which allows us to manipulate functions as first-class values. One of the benefits of reification of assertions into "arrows" is that we can write macros to generate assertions from pure Scala code. This is how the smart assertions work in ZIO Test. ::: ## Testing using Assertions We have two types of methods for writing test assertions: 1. **[Classic Assertions](classic-assertions.md)**— This one is the classic way of asserting ordinary values (`assert`) and ZIO effects (`assertZIO`) without using macros. 2. **[Smart Assertions](smart-assertions.md)**— This is a unified syntax for asserting both ordinary values and ZIO effects using the `assertTrue` macro. --- ## Smart Assertions The smart assertion is a simple way to assert both _ordinary values_ and _ZIO effects_. It uses the `assertTrue` function, which uses macro under the hood. ## Asserting Ordinary Values In the following example, we assert simple ordinary values using the `assertTrue` method: ```scala test("sum"){ assertTrue(1 + 1 == 2) } ``` We can assert multiple assertions inside a single `assertTrue`: ```scala test("multiple assertions"){ assertTrue( true, 1 + 1 == 2, Some(1 + 1) == Some(2) ) } ``` ## Asserting ZIO effects The `assertTrue` method can also be used to assert ZIO effects: ```scala test("updating ref") { for { r <- Ref.make(0) _ <- r.update(_ + 1) v <- r.get } yield assertTrue(v == 1) } ``` Using `assertTrue` with for-comprehension style, we can think of testing as these three steps: 1. **Set up the test** — In this section we should setup the system under test (e.g. `Ref.make(0)`). 2. **Running the test** — Then we run the test scenario according to the test specification. (e.g `ref.update(_ + 1)`) 3. **Making assertions about the test** - Finally, we should assert the result with the right expectations (e.g. `assertTrue(v == 1)`) ## Assertion Operators Each `assertTrue` returns a `AssertResult`, so they have the same operators as `AssertResult`. Here are some of the useful operators: 1. **`&&`** - This is the logical and operator to make sure that both assertions are true: ```scala test("&&") { check(Gen.int <*> Gen.int) { case (x: Int, y: Int) => assertTrue(x + y == y + x) && assertTrue(x * y == y * x) } } ``` 2. **||** - This is the logical or operator to make sure that at least one of the assertions is true: ```scala suite("||")( test("false || true") { assertTrue(false) || assertTrue(true) // this will pass }, test("true || false") { assertTrue(true) || assertTrue(false) // this will pass }, test("true || true") { assertTrue(true) || assertTrue(true) // this will pass }, test("false || false") { assertTrue(false) || assertTrue(false) // this will false }, ) ``` 3. **`!`** - This is the logical not operator to negate the assertion: ```scala suite("unary !") ( test("negate true") { !assertTrue(true) // this will fail }, test("negate false") { !assertTrue(false) // this will pass } ) ``` 4. **implies** - This is the logical implies operator to make sure that the first assertion implies the second assertion. It is equivalent to `!p || q` which is a conditional statement of the form "if p, then q" where p and q are propositions. The `==>` operator is an alias for `implies`. ```scala suite("implies") ( test("true implies true")( assertTrue(true) implies assertTrue(true) // this will pass ), test("true implies false")( assertTrue(true) implies assertTrue(false) // this will fail ), test("false implies true")( assertTrue(false) implies assertTrue(true) // this will pass ), test("false implies false")( assertTrue(false) implies assertTrue(false) // this will pass ), ) ``` The `implies` assertion is true if either the p is false or when both p and q are true: | P | Q | P implies Q | |-------|-------|-------------| | true | true | true | | true | false | false | | false | true | true | | false | false | true | 5. **iff** - This is the logical iff operator to make sure that the first assertion is true if and only if the second assertion is true. It is equivalent to `(p implies q) && (q implies p)`. The `<==>` operator is an alias for `iff`. ```scala suite("iff") ( test("true iff true")( assertTrue(true) iff assertTrue(true) // this will pass ), test("true iff false")( assertTrue(true) iff assertTrue(false) // this will fail ), test("false iff true")( assertTrue(false) iff assertTrue(true) // this will fail ), test("false iff false")( assertTrue(false) iff assertTrue(false) // this will pass ) ) ``` Here is the truth table for the iff operator: | P | Q | P iff Q | |-------|-------|---------| | true | true | true | | true | false | false | | false | true | false | | false | false | true | 6. **??**- We can add a custom message to the assertion using the `??` operator. This will be useful when assertion fails, and we want to provide more information about the failure: ```scala assertTrue(1 + 1 == 3) ?? "1 + 1 should be equal to 2" ``` ## Asserting Nested Values There are several operators designed specifically for use within the `assertTrue` macro, enhancing the ease and readability of assertions. These operators, intended exclusively for the `assertTrue` macro, leverage the `TestLens[A]` type-class to access the underlying value of the type `A`. We use the `is` extension method inside the `assertTrue` macro to convert the given value to a `TestLens`. Now no matter how deeply nested the value is, we can access the underlying values using extension method defined for `TestLens` values: ### Testing Optional Values There are two operators for testing optional values: 1. **`TestLens#some`** - This operator is used to peek into the `Some` value: ```scala test("optional value is some(42)") { val sut: Option[Int] = Some(40 + 2) assertTrue(sut.is(_.some) == 42) } ``` 2. **`TestLens#anything`** - This operator is used to assert that the value is `Some`: ```scala test("optional value is anything") { val sut: Option[Int] = Some(42) assertTrue(sut.is(_.anything)) } ``` ### Testing Either Values 1. **`TestLens#right`** - This operator is used to peek into the `Right` value: ```scala test("TestLens#right") { val sut: Either[Error, Int] = Right(40 + 2) assertTrue(sut.is(_.right) == 42) } ``` 2. **`TestLens#left`** - This operator is used to peek into the `Left` value: ```scala case class Error(errorMessage: String) test("TestLens#left") { val sut: Either[Error, Int] = Left(Error("Boom!")) assertTrue(sut.is(_.left).errorMessage == "Boom!") } ``` 3. **`TestLens#anything`** - This operator is used to assert that the value is `Right`: ```scala test("TestLens#anything") { val sut: Either[Error, Int] = Right(42) assertTrue(sut.is(_.anything)) } ``` ### Testing Exit Values 1. **`TestLens#success`** - This operator transforms the `Exit` value to its success type `A` if it is a `Exit.Success`, otherwise it will fail. So this can be used for asserting the success value of the `Exit`: ```scala test("TestLens#success") { val sut: Exit[Error, Int] = Exit.succeed(42) assertTrue(sut.is(_.success) == 42) } ``` 2. **`TestLens#failure`** - This operator transforms the `Exit` value to its failure type `E` if it is a `Exit.Failure`, otherwise it will fail. So this can be used for asserting the failure value of the `Exit`: ```scala case class Error(errorMessage: String) test("TestLens#failure") { val sut: Exit[Error, Int] = Exit.fail(Error("Boom!")) assertTrue(sut.is(_.failure).errorMessage == "Boom!") } ``` 3. **`TestLens#die`** - This operator transforms the `Exit` value to its die type `E` if it is a `Exit.Die`, otherwise it will fail. So this can be used for asserting the die value of the `Exit`: ```scala test("TestLens#die") { val sut: Exit[Error, Int] = Exit.die(new RuntimeException("Boom!")) assertTrue(sut.is(_.die).getMessage == "Boom!") } ``` 4. **`TestLens#cause`** - This operator transforms the `Exit` value to its underlying `Cause` value if it has one otherwise it will fail. So this can be used for asserting the cause of the `Exit`: ```scala test("TestLens#cause") { for { exit <- ZIO.failCause(Cause.fail("Boom!")).exit } yield assertTrue(exit.is(_.cause) == Cause.fail("Boom!")) } // error: Error is already defined as case class Error ``` 5. **`TestLens#interrupt`** - This operator transforms the `Exit` value to its interrupt value if it is a `Exit.Interrupt`, otherwise it will fail. So this can be used for asserting the interrupt value of the `Exit`: ```scala test("TestLens#interrupt") { for { exit <- ZIO.sleep(5.seconds).fork.flatMap(_.interrupt) } yield assertTrue(exit.is(_.interrupted)) } ``` ## Deeply Nested Values Sometimes we need to test values with more than one level of nesting. There is no difference in the way we test nested values: ```scala test("assertion of multiple nested values (TestLens#right.some)") { val sut: Either[Error, Option[Int]] = Right(Some(40 + 2)) assertTrue(sut.is(_.right.some) == 42) } ``` ## Custom Assertions Using `CustomAssertion` we can create our own custom assertions for use in `assertTrue`. We can define custom assertions using the `CustomAssertion.make` method. This method takes a partial function from the type `A` to `Either[String, B]`. If the partial function is defined for the given value, it returns `Right[B]`, otherwise it returns `Left[String]`. Here is an example of a custom assertion for a sealed trait and case classes: ```scala // Define the sealed trait and case classes sealed trait Book case class Novel(pageCount: Int) extends Book case class Comic(illustrations: Int) extends Book case class Textbook(subject: String) extends Book // Custom assertion for Book val subject = CustomAssertion.make[Book] { case Textbook(subject) => Right(subject) case other => Left(s"Expected $$other to be Textbook") } // Usage suite("custom assertions")( test("subject assertion") { val book: Option[Book] = Some(Textbook("Mathematics")) assertTrue(book.is(_.some.custom(subject)) == "Mathematics") } ) ``` In the above example, we define a custom assertion for the `Book` sealed trait. The custom assertion `subject` is defined to extract the `subject` from the `Textbook` case class. So then we can assert the `subject` of the `Textbook` case class. ## More Examples The `assertTrue` macro is designed to make it easy to write assertions in a more readable way. Most test cases can be written as when we're comparing ordinary values in Scala. However, we have a [`SmartAssertionSpec`](https://github.com/zio/zio/blob/series/2.x/test-tests/shared/src/test/scala/zio/test/SmartAssertionSpec.scala) which is a collection of examples to demonstrate the power of the `assertTrue` macro. --- ## zio.test.diff.Diff When asserting two things are the same it's sometimes difficult to see the difference. Luckily there is a `zio.test.Diff` type-class. The purpose this type class is to output the difference between two things. This can be one of the primitives types like `String`, `Int`, `Double`, etc. But also more complex structures like a `Map`, `List` and so-forth. ### Derive for case classes and algebraic data types To _derive_ a type-class for a case class or a algebraic data type you can include the module `zio-test-magnolia` if it's not included already. Which includes `DeriveDiff` and `DeriveGen` as well. To make it work you need to import the `DeriveDiff` object/trait: ```scala ``` An example of a difference output inside a test may look like this ``` ✗ There was a difference Expected Person( name = "Bibi", nickname = Some("""Bibbo The Bibber Bobber"""), age = 300, pet = Pet( name = "The Beautiful Destroyer", hasBone = false, favoriteFoods = List("Alpha", "This is a wonderful way to live and die", "Potato", "Brucee Lee", "Potato", "Ziverge"), birthday = 2023-08-20T17:32:33.479852Z ), person = Some(Person( name = "Bibi", nickname = Some("""Bibbo The Bibber Bobber"""), age = 300, pet = Pet( name = "The Beautiful Destroyer", hasBone = false, favoriteFoods = List("Alpha", "This is a wonderful way to live and die", "Potato", "Brucee Lee", "Potato", "Ziverge"), birthday = 2023-08-20T17:32:33.479855Z ), person = None )) ) Diff -expected +obtained Person( name = "Bibi" → "Boboo", nickname = Some( """Bibbo The Bibber Bobber""" → """Babbo The Bibber""" ), pet = Pet( name = "The Beautiful Destroyer" → "The Beautiful Crumb", favoriteFoods = List( 1 = "This is a wonderful way to live and die" → "This is a wonderful \"way\" to dance and party", 3 = "Brucee Lee", 4 = "Potato", 5 = "Ziverge" ), birthday = 2023-08-20T17:32:33.479852Z → -1000000000-01-01T00:00:00Z ), person = Some( Person( name = "Bibi" → "Boboo", nickname = Some( """Bibbo The Bibber Bobber""" → """Babbo The Bibber""" ), pet = Pet( name = "The Beautiful Destroyer" → "The Beautiful Crumb", favoriteFoods = List( 1 = "This is a wonderful way to live and die" → "This is a wonderful \"way\" to dance and party", 3 = "Brucee Lee", 4 = "Potato", 5 = "Ziverge" ), birthday = 2023-08-20T17:32:33.479855Z → -1000000000-01-01T00:00:00Z ) ) ) ) p1 == p2 p1 = Person( name = "Boboo", nickname = Some("""Babbo The Bibber"""), age = 300, pet = Pet( name = "The Beautiful Crumb", hasBone = false, favoriteFoods = List("Alpha", "This is a wonderful \"way\" to dance and party", "Potato"), birthday = -1000000000-01-01T00:00:00Z ), person = Some(Person( name = "Boboo", nickname = Some("""Babbo The Bibber"""), age = 300, pet = Pet( name = "The Beautiful Crumb", hasBone = false, favoriteFoods = List("Alpha", "This is a wonderful \"way\" to dance and party", "Potato"), birthday = -1000000000-01-01T00:00:00Z ), person = None )) ) ``` ### Custom types For more custom types you could provide type-class instances your self by implementing the `zio.test.diff.Diff` type-class. ```scala // somewhere defined in your domain package case class Percentage(repr: Int) implicit val diffPercentage: Diff[Percentage] = Diff[Double].contramap(_.repr) ``` ### Be wary of `LowPriDiff` One thing to note that there is a trait `LowPriDiff` which is stacked on the companion object of `zio.test.diff.Diff`. There is lower priority type-class instance defined at `LowerPriDiff` which is a fallback for `AnyVal`. It's defined as `implicit def anyValDiff[A <: AnyVal]: Diff[A] = anyDiff[A]`, so if some custom types mess up your diff, you might want to check on this topic. --- ## Dynamic Test Generation Tests in ZIO are dynamic. Meaning that they are not required to be statically defined at compile time. They can be generated at runtime effectfully. Assume we have implemented the `add` operator which adds two numbers: ```scala def add(a: Int, b: Int): Int = ??? ``` We want to test this function using the following test data inside the `resources` directory: ```scala title="src/test/resources/test-data.csv" 0, 0, 0 1, 0, 1 0, 1, 1 0, -1, -1 -1, 0, -1 1, 1, 2 1, -1, 0 -1, 1, 0 ``` Let's load it and create a bunch of tests using this test data: ```scala def loadTestData: Task[List[((Int, Int), Int)]] = ZIO.attemptBlocking( scala.io.Source .fromResource("test-data.csv") .getLines() .toList .map(_.split(',').map(_.trim)) .map(i => ((i(0).toInt, i(1).toInt), i(2).toInt)) ) def makeTest(a: Int, b: Int)(expected: Int): Spec[Any, Nothing] = test(s"test add($a, $b) == $expected") { assertTrue(add(a, b) == expected) } def makeTests: ZIO[Any, Throwable, List[Spec[Any, Nothing]]] = loadTestData.map { testData => testData.map { case ((a, b), expected) => makeTest(a, b)(expected) } } ``` Now we are ready to run all generated tests: ```scala object AdditionSpec extends ZIOSpecDefault { override def spec = suite("add")(makeTests) } ``` Here is the test runner's output: ```scala + add + test add(0, 0) == 0 + test add(1, 0) == 1 + test add(0, -1) == -1 + test add(0, 1) == 1 + test add(-1, 1) == 0 + test add(1, -1) == 0 + test add(1, 1) == 2 + test add(-1, 0) == -1 8 tests passed. 0 tests failed. 0 tests ignored. ``` --- ## Introduction to ZIO Test **ZIO Test** is a zero dependency testing library that makes it easy to test effectual programs. In **ZIO Test**, all tests are immutable values and tests are tightly integrated with ZIO, so testing effectual programs is as natural as testing pure ones. ## Motivation We can easily assert ordinary values and data types to test them: ```scala assert(1 + 2 == 2 + 1) assert("Hi" == "H" + "i") case class Point(x: Long, y: Long) assert(Point(5L, 10L) == Point.apply(5L, 10L)) ``` What about functional effects? Can we assert two effects using ordinary scala assertion to test whether they have the same functionality? As we know, a functional effect, like `ZIO`, describes a series of computations. Unfortunately, we can't assert functional effects without executing them. If we assert two `ZIO` effects, e.g. `assert(expectedEffect == actualEffect)`, the result says nothing about whether these two effects behave similarly and produce the same result or not. Instead, we should `unsafeRun` each one and assert their results. Let's say we have a random generator effect, and we want to ensure that the output is bigger than zero, so we should `unsafeRun` the effect and assert the result: ```scala val random = Unsafe.unsafe { implicit unsafe => Runtime.default.unsafe.run( Random.nextIntBounded(10) ).getOrThrowFiberFailure() } assert(random >= 0) ``` Testing effectful programs is difficult since we should use many `unsafeRun` methods. Also, we might need to make sure that the test is non-flaky. In these cases, running `unsafeRun` multiple times is not straightforward. We need a testing framework that treats effects as _first-class values_. So this is the primary motivation for creating the ZIO Test library. ## How ZIO Test was designed We designed ZIO Test around the idea of _making tests first-class objects_. This means that tests (and other concepts, like assertions) become ordinary values that can be passed around, transformed, and composed. This approach allows for greater flexibility compared to some other testing frameworks, where tests and additional logic around tests had to be put into callbacks so that framework could make use of them. As a result, this approach is also better suited to other `ZIO` concepts like `Scope`, which can only be used within a scoped block of code. This also created a mismatch between `BeforeAll`, `AfterAll` callback-like methods when there were resources that should be opened and closed during test suite execution. Another thing worth pointing out is that tests being values are also effects. Implications of this design are far-reaching: 1. First, the well-known problem of testing asynchronous value is gone. Whereas in other frameworks we have to somehow "run" our effects and at best wrap them in `scala.util.Future` because blocking would eliminate running on ScalaJS, ZIO Test expects us to create `ZIO` objects. There is no need for indirect transformations from one wrapping object to another. 2. Second, because our tests are ordinary `ZIO` values, we don't need to turn to a testing framework for things like retries, timeouts, and resource management. We can solve all those problems with the full richness of functions that `ZIO` exposes. --- ## Installing ZIO Test In order to use ZIO Test, we need to add the required configuration in our SBT settings: ```scala libraryDependencies ++= Seq( "dev.zio" %% "zio-test" % "2.1.26" % Test, "dev.zio" %% "zio-test-sbt" % "2.1.26" % Test, "dev.zio" %% "zio-test-magnolia" % "2.1.26" % Test ) ``` If our SBT version is older than 1.8.0, we also need to add the test framework manually: ```scala testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") ``` **NOTE**: In order to use the live version of a service in our tests, we can use some new helpful test aspects e.g `withLiveClock`, `withLiveConsole`, `withLiveRandom`, `withLiveSystem`, etc. --- ## Integrating ZIO Test with JUnit Unit testing is an essential practice in software development, enabling developers to validate the correctness and reliability of their code. JUnit, a widely adopted testing framework, has emerged as a standard choice for Java applications. With its robust features and extensive ecosystem, JUnit simplifies the process of writing and executing tests, empowering developers to deliver high-quality software. In this section, we will explore the integration of ZIO Test, a powerful testing library for functional programming in Scala, with JUnit. By combining the strengths of both frameworks, developers can efficiently test ZIO-based applications under different build tools and IDEs. To streamline the testing process, a custom JUnit runner is provided specifically for running ZIO Test specifications. Thus, we can conduct testing of ZIO specs within alternative build tools, such as Maven, Gradle, Bazel, and various integrated development environments (IDEs). By adding the necessary dependency definition to the build tool, developers can effortlessly incorporate the ZIO Test JUnit runner: ```scala libraryDependencies += "dev.zio" %% "zio-test-junit" % zioVersion % "test" ``` To make our spec appear as a JUnit test to build tools and IDEs, we can simply extend `zio.test.junit.JUnitRunnableSpec`: ```scala object MySpec extends JUnitRunnableSpec { def spec = suite("MySpec")( test("test") { for { _ <- ZIO.unit } yield assertCompletes } ) } ``` Now, we can run our spec from the command line by running `sbt test`: ```bash sbt:zio-quickstart-junit> test + MySpec + test 1 tests passed. 0 tests failed. 0 tests ignored. Executed in 215 ms [info] Completed tests [success] Total time: 1 s, completed Jun 13, 2023, 4:39:27 PM ``` Or we can convert `MySpec` object to a scala `class` and annotate it with `@RunWith(classOf[ZTestJUnitRunner])`: ```scala @RunWith(classOf[ZTestJUnitRunner]) class MySpec extends ZIOSpecDefault { def spec = suite("MySpec")( test("test") { for { _ <- ZIO.unit } yield assertCompletes } ) } ``` To run the above test using `sbt test` we also need to add the following line to our `build.sbt`: ```scala libraryDependencies += "com.github.sbt" % "junit-interface" % "0.13.3" % Test ``` Now, we can run our spec by running `sbt test` from the command line: ```bash sbt:zio-quickstart-junit> test + MySpec + test [info] Passed: Total 1, Failed 0, Errors 0, Passed 1 [success] Total time: 1 s, completed Jun 13, 2023, 4:37:32 PM ``` To see practical examples in action, we encourage you to check out [the example code base](https://github.com/zio/zio-quickstarts/tree/master/zio-quickstart-junit-integration) provided in the [ZIO Quickstarts](https://github.com/zio/zio-quickstarts/) project on GitHub. This code base will help you dive deeper into testing ZIO specs using JUnit within different build tools and IDEs, enabling you to enhance the quality and stability of your ZIO applications. Happy testing! --- ## Built-in Generators In the companion object of the `Gen` data type, there are tons of generators for various data types. ## Primitive Types Generators ZIO Test provides generators for primitive types such as `Gen.int`, `Gen.string`, `Gen.boolean`, `Gen.float`, `Gen.double`, `Gen.bigInt`, `Gen.byte`, `Gen.bigDecimal`, `Gen.long`, `Gen.char`, and `Gen.short`. Let's create an `Int` generator: ```scala val intGen: Gen[Any, Int] = Gen.int ``` ## Character Generators In addition to `Gen.char`, ZIO Test offers a variety of specialized character generators: * `Gen.alphaChar` — e.g. `Z, z, A, t, o, e, K, E, y, N` * `Gen.alphaNumericChar` — e.g. `b, O, X, B, 4, M, k, 9, a, p` * `Gen.asciiChar` — e.g. `, >, , , , 2, k, , , ` * `Gen.unicodeChar` — e.g. `了, , 옷, 嗀, , 뮲, ﹓, 癮, , ᜣ)` * `Gen.numericChar` — e.g. `1, 0, 1, 5, 6, 9, 4, 4, 5, 2` * `Gen.printableChar` — e.g. `H, J, (, Q, n, g, 4, G, 9, l` * `Gen.whitespaceChars` — e.g. `, ,  , , ,  ,  , ,  , ` * `Gen.hexChar` — e.g. `3, F, b, 5, 9, e, 2, 8, b, e` * `Gen.hexCharLower` — e.g. `f, c, 4, 4, c, 2, 5, 4, f, 3` * `Gen.hexCharUpper` — e.g. `4, 8, 9, 8, C, 9, F, A, E, C` ## String Generators Besides the primitive string generator, `Gen.string`, ZIO Test also provides the following specialized generators: 1. `Gen.stringBounded` — A generator of strings whose size falls within the specified bounds: ```scala mdoc:compile-only Gen.stringBounded(1, 5)(Gen.alphaChar) .runCollectN(10) .debug // Sample Output: List(b, YJXzY, Aro, y, WMPbj, Abxt, kJep, LKN, kUtr, xJ) ``` 2. `Gen.stringN` — A generator of strings of fixed size: ```scala mdoc:compile-only Gen.stringN(5)(Gen.alphaChar) .runCollectN(10) .debug // Sample Output: List(BuywQ, tXCEy, twZli, ffLwI, BPEbz, OKYTi, xeDJW, iDUVn, cuMCr, keQAA) ``` 3. `Gen.string1` — A generator of strings of at least one character. 4. `Gen.alphaNumericString` — A generator of alphanumeric characters. 5. `Gen.alphaNumericStringBounded` — A generator of alphanumeric strings whose size falls within the specified bounds. 6. `Gen.iso_8859_1` — A generator of strings that can be encoded in the ISO-8859-1 character set. 7. `Gen.asciiString` — A generator of US-ASCII characters. ## Generating Fixed Values 1. `Gen.const` — A constant generator of the specified value. ```scala mdoc:compile-only Gen.const(true).runCollectN(5) // Output: List(true, true, true, true, true) ``` 2. `Gen.constSample` — A constant generator of the specified sample: ```scala mdoc:compile-only Gen.constSample(Sample.noShrink(false)).runCollectN(5) // Output: List(true, true, true, true, true) ``` 3. `Gen.unit` — A constant generator of the unit value. 4. `Gen.throwable` — A generator of throwables. Note that there is an empty generator called `Gen.empty`, which generates no values and returns nothing. We can think of that as a generator of empty stream, `Gen(Stream.empty)`. ## Generating from Fixed Values 1. `Gen.elements` — Constructs a non-deterministic generator that only generates randomly from the fixed values: ```scala Gen.elements( DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY, DayOfWeek.SATURDAY, DayOfWeek.SUNDAY ).runCollectN(3).debug // Sample Output: List(WEDNESDAY, THURSDAY, SUNDAY) ``` 2. `Gen.fromIterable` — Constructs a deterministic generator that only generates the specified fixed values: ```scala Gen.fromIterable(List("red", "green", "blue")) .runCollectN(10) .debug ``` ## Collection Generators ZIO Test has generators for collection data types such as _sets_, _lists_, _vectors_, _chunks_, and _maps_. These data types share similar APIs. The following example illustrates how the generator of sets works: ```scala // A sized generator of sets Gen.setOf(Gen.alphaChar) // Sample Output: Set(Y, M, c), Set(), Set(g, x, Q), Set(s), Set(f, J, b, R) // A sized generator of non-empty sets Gen.setOf1(Gen.alphaChar) // Sample Output: Set(Y), Set(L, S), Set(i), Set(H), Set(r, Z, z) // A generator of sets whose size falls within the specified bounds. Gen.setOfBounded(1, 3)(Gen.alphaChar) // Sample Output: Set(Q), Set(q, J), Set(V, t, h), Set(c), Set(X, O) // A generator of sets of the specified size. Gen.setOfN(2)(Gen.alphaChar) // Sample Output: Set(J, u), Set(u, p), Set(i, m), Set(b, N), Set(B, Z) ``` ## Bounded Generator The `Gen.bounded` constructor is a generator whose size falls within the specified bounds: ```scala Gen.bounded(2, 5)(Gen.stringN(_)(Gen.alphaChar)) .runCollectN(5) ``` ## Suspended Generator The `Gen.suspend` constructs a generator lazily. This is useful to avoid infinite recursion when creating generators that refer to themselves. ## Unfold Generator The `unfoldGen` takes the initial state and depending on the previous state, it determines what will be the next generated value: ```scala def unfoldGen[R, S, A](s: S)(f: S => Gen[R, (S, A)]): Gen[R, List[A]] ``` Assume we want to test the built-in scala stack (`scala.collection.mutable.Stack`). One way to do that is to create an acceptable series of push and pop commands, and then check that the stack doesn't throw any exception by executing these commands: ```scala sealed trait Command case object Pop extends Command final case class Push(value: Char) extends Command val genPop: Gen[Any, Command] = Gen.const(Pop) def genPush: Gen[Any, Command] = Gen.alphaChar.map(Push) val genCommands: Gen[Any, List[Command]] = Gen.unfoldGen(0) { n => if (n <= 0) genPush.map(command => (n + 1, command)) else Gen.oneOf( genPop.map(command => (n - 1, command)), genPush.map(command => (n + 1, command)) ) } ``` We are now ready to test the generated list of commands: ```scala test("unfoldGen") { check(genCommands) { commands => val stack = scala.collection.mutable.Stack.empty[Int] commands.foreach { case Pop => stack.pop() case Push(value) => stack.push(value) } assertCompletes } } ``` ## From a ZIO Effect 1. `Gen.fromZIO` ```scala mdoc:compile-only val gen: Gen[Any, Int] = Gen.fromZIO(Random.nextInt) ``` 2. `Gen.fromZIOSample` ```scala mdoc:compile-only val gen: Gen[Any, Int] = Gen.fromZIOSample( Random.nextInt.map(Sample.shrinkIntegral(0)) ) ``` ## From a Random Effect 3. `Gen.fromRandom` — Constructs a generator from a function that uses randomness: ```scala mdoc:compile-only val gen: Gen[Any, Int] = Gen.fromRandom(_.nextInt) ``` 4. `Gen.fromRandomSample` — Constructs a generator from a function that uses randomness to produce a sample: ```scala mdoc:compile-only val gen: Gen[Any, Int] = Gen.fromRandomSample( _.nextIntBounded(20).map(Sample.shrinkIntegral(0)) ) ``` ## Uniform and Non-uniform Generators 1. `Gen.uniform` — A generator of uniformly distributed doubles between [0, 1]. 2. `Gen.weighted` — A generator which chooses one of the given generators according to their weights. For example, the following generator will generate 90% true and 10% false values: ```scala mdoc:compile-only val trueFalse = Gen.weighted((Gen.const(true), 9), (Gen.const(false), 1)) trueFalse.runCollectN(10).debug // Sample Output: List(false, false, false, false, false, false, false, false, true, false) ``` 3. `Gen.exponential` — A generator of exponentially distributed doubles with mean `1`: ```scala mdoc:compile-only Gen.exponential.map(x => math.round(x * 100) / 100.0) .runCollectN(10) .debug // Sample Output: List(0.22, 3.02, 1.96, 1.13, 0.81, 0.92, 1.7, 1.47, 1.55, 0.46) ``` ## Generating Date/Time Types | Date/Time Types | Generators | |----------------------------|----------------------| | `java.time.DayOfWeek` | `Gen.dayOfWeek` | | `java.time.Month` | `Gen.month` | | `java.time.Year` | `Gen.year` | | `java.time.Instant` | `Gen.instant` | | `java.time.MonthDay` | `Gen.monthDay` | | `java.time.YearMonth` | `Gen.yearMonth` | | `java.time.ZoneId` | `Gen.zoneId` | | `java.time.ZoneOffset` | `Gen.zoneOffset` | | `java.time.ZonedDateTime` | `Gen.zonedDateTime` | | `java.time.OffsetTime` | `Gen.offsetTime` | | `java.time.OffsetDateTime` | `Gen.offsetDateTime` | | `java.time.Period` | `Gen.period` | | `java.time.LocalDate` | `Gen.localDate` | | `java.time.LocalDateTime` | `Gen.localDateTime` | | `java.time.LocalTime` | `Gen.localTime` | | `zio.duration.Duration` | `Gen.finiteDuration` | ## Function Generators To test some properties, we need to generate functions. There are two types of function generators: 1. `Gen.function` — It takes a generator of type `B` and produces a generator of functions from `A` to `B`: ```scala def function[R, A, B](gen: Gen[R, B]): Gen[R, A => B] ``` Two `A` values will be considered to be equal, and thus will be guaranteed to generate the same `B` value, if they have the same `hashCode`. 2. `Gen.functionWith` — It takes a generator of type `B` and also a hash function for `A` values, and produces a generator of functions from `A` to `B`: ```scala def functionWith[R, A, B](gen: Gen[R, B])(hash: A => Int): Gen[R, A => B] ``` Two `A` values will be considered to be equal, and thus will be guaranteed to generate the same `B` value, if they have the same hash. This is useful when `A` does not implement `hashCode` in a way that is consistent with equality. Accordingly, ZIO Test provides a variety of function generators for `Function2`, `Function3`, ..., and also the `PartialFunction`: * `Gen.function2` — Gen[R, C] => Gen[R, (A, B) => C] * `Gen.functionWith2` — Gen[R, B] => ((A, B) => Int) => Gen[R, (A, B) => C] * `Gen.partialFunction` — Gen[R, B] => Gen[R, PartialFunction[A, B]] * `Gen.partialFunctionWith` — Gen[R, B] => (A => Int) => Gen[R, PartialFunction[A, B]] Let's write a test for `ZIO.foldLeft` operator. This operator has the following signature: ```scala def foldLeft[R, E, S, A](in: => Iterable[A])(zero: => S)(f: (S, A) => ZIO[R, E, S]): ZIO[R, E, S] ``` We want to test the following property: ```scala ∀ (in, zero, f) => ZIO.foldLeft(in)(zero)(f) == ZIO(List.foldLeft(in)(zero)(f)) ``` To test this property, we have an input of type `(Int, Int) => Int`. So we need a Function2 generator of integers: ```scala val func2: Gen[Any, (Int, Int) => Int] = Gen.function2(Gen.int) ``` Now we can test this property: ```scala test("ZIO.foldLeft should have the same result with List.foldLeft") { check(Gen.listOf(Gen.int), Gen.int, func2) { case (in, zero, f) => assertZIO( ZIO.foldLeft(in)(zero)((s, a) => ZIO.attempt(f(s, a))) )(Assertion.equalTo( in.foldLeft(zero)((s, a) => f(s, a))) ) } } ``` ## Generating ZIO Values 1. Successful effects (`Gen.successes`): ```scala mdoc:compile-only val gen: Gen[Any, UIO[Int]] = Gen.successes(Gen.int(-10, 10)) ``` 2. Failed effects (`Gen.failures`): ```scala mdoc:compile-only val gen: Gen[Any, IO[String, Nothing]] = Gen.failures(Gen.string) ``` 3. Died effects (`Gen.died`): ```scala mdoc:compile-only val gen: Gen[Any, UIO[Nothing]] = Gen.died(Gen.throwable) ``` 4. Cause values (`Gen.causes`): ```scala mdoc:compile-only val causes: Gen[Any, Cause[String]] = Gen.causes(Gen.string, Gen.throwable) ``` 5. Chained effects (`Gen.chained`, `Gen.chainedN`): A generator of effects that are the result of chaining the specified effect with itself a random number of times. Let's see some example of chained ZIO effects: ```scala mdoc:compile-only val effect1 = ZIO(2).flatMap(x => ZIO(x * 2)) val effect2 = ZIO(1) *> ZIO(2) ``` By using `Gen.chained` or `Gen.chainedN` generator, we can create generators of chained effects: ```scala mdoc:compile-only val chained : Gen[Any, ZIO[Any, Nothing, Int]] = Gen.chained(Gen.successes(Gen.int)) val chainedN: Gen[Any, ZIO[Any, Nothing, Int]] = Gen.chainedN(5)(Gen.successes(Gen.int)) ``` 6. Concurrent effects (`Gen.concurrent`): A generator of effects that are the result of applying concurrency combinators to the specified effect that are guaranteed not to change its value. ```scala mdoc:compile-only val random : Gen[Any, UIO[Int]] = Gen.successes(Gen.int).flatMap(Gen.concurrent) val constant: Gen[Any, UIO[Int]] = Gen.concurrent(ZIO(3)) ``` 7. Parallel effects (`Gen.parallel`): A generator of effects that are the result of applying parallelism combinators to the specified effect that are guaranteed not to change its value. ```scala mdoc:compile-only val random: Gen[Any, UIO[String]] = Gen.successes(Gen.string).flatMap(Gen.parallel) val constant: Gen[Any, UIO[String]] = Gen.parallel(ZIO("Hello")) ``` ## Generating Compound Types 1. tuples — We can combine generators using for-comprehension syntax and tuples: ```scala mdoc:compile-only val tuples: Gen[Any, (Int, Double)] = for { a <- Gen.int b <- Gen.double } yield (a, b) ``` 2. `Gen.oneOf` — It takes variable number of generators and select one of them: ```scala mdoc:compile-only sealed trait Color case object Red extends Color case object Blue extends Color case object Green extends Color Gen.oneOf(Gen.const(Red), Gen.const(Blue), Gen.const(Green)) // Sample Output: Green, Green, Red, Green, Red ``` 4. `Gen.option` — A generator of _optional_ values: ```scala mdoc:compile-only val intOptions: Gen[Any, Option[Int]] = Gen.option(Gen.int) val someInts: Gen[Any, Option[Int]] = Gen.some(Gen.int) val nons: Gen[Any, Option[Nothing]] = Gen.none ``` 3. `Gen.either` — A generator of _either_ values: ```scala mdoc:compile-only val char: Gen[Any, Either[Char, Char]] = Gen.either(Gen.numericChar, Gen.alphaChar) ``` 4. `Gen.collectAll` — Composes the specified generators to create a _cartesian product of elements_ with the specified function: ```scala mdoc:compile-only val gen: ZIO[Any, Nothing, List[List[Int]]] = Gen.collectAll( List( Gen.fromIterable(List(1, 2)), Gen.fromIterable(List(3)), Gen.fromIterable(List(4, 5)) ) ).runCollect // Output: // List( // List(1, 3, 4), // List(1, 3, 5), // List(2, 3, 4), // List(2, 3, 5) //) ``` 5. `Gen.concatAll` — Combines the specified deterministic generators to return a new deterministic generator that generates all the values generated by the specified generators: ```scala mdoc:compile-only val gen: ZIO[Any, Nothing, List[Int]] = Gen.concatAll( List( Gen.fromIterable(List(1, 2)), Gen.fromIterable(List(3)), Gen.fromIterable(List(4, 5)) ) ).runCollect // Output: List(1, 2, 3, 4, 5) ``` ## Sized Generators 1. `Gen.sized` — A sized generator takes a function from `Int` to `Gen[R, A]` and creates a generator by applying a size to that function: ```scala mdoc:compile-only Gen.sized(Gen.int(0, _)) .runCollectN(10) .provideCustomLayer(Sized.live(5)) .debug // Sample Output: List(5, 4, 1, 2, 0, 4, 2, 0, 1, 2) ``` 2. `Gen.size` — A generator which accesses the _size_ from the environment and generates that: ```scala mdoc:compile-only Gen.size .runCollectN(5) .provideCustomLayer(Sized.live(100)) .debug // Output: List(100, 100, 100, 100, 100) ``` There are also three sized generators, named _small_, _medium_ and _large_, that use an exponential distribution of size values: 1. `Gen.small` — The values generated will be strongly concentrated towards the lower end of the range but a few larger values will still be generated: ```scala mdoc:compile-only Gen.small(Gen.const(_)) .runCollectN(10) .provideCustomLayer(Sized.live(1000)) .debug // Output: List(6, 39, 73, 3, 57, 51, 40, 12, 110, 46) ``` 4. `Gen.medium` — The majority of sizes will be towards the lower end of the range but some larger sizes will be generated as well: ```scala mdoc:compile-only Gen.medium(Gen.const(_)) .runCollectN(10) .provideCustomLayer(Sized.live(1000)) .debug // Output: List(93, 42, 58, 228, 42, 5, 12, 214, 106, 79) ``` 5. `Gen.large` — Uses a uniform distribution of size values. A large number of larger sizes will be generated: ```scala mdoc:compile-only Gen.large(Gen.const(_)) .runCollectN(10) .provideCustomLayer(Sized.live(1000)) .debug // Output: List(797, 218, 596, 278, 301, 779, 165, 486, 695, 788) ``` --- ## Getting Started With Property Checking The fundamental idea behind property checking is to test the properties of the target function using random inputs. So to test a system using property checking, two things are required: 1. Properties 2. Generators A property of a system is a predicate that is always true regardless of the system's input. For example, the addition of two numbers is commutative. So it doesn't matter what numbers we pass to the addition function, for any pair of `a` and `b`, the result of `add(a, b)` is always the same as `add(b, a)`: ```scala def add(a: Int, b: Int): Int = ??? def is_add_commutative(a: Int, b: Int): Boolean = add(a, b) == add(b, a) ``` The `is_add_commutative` predicate takes two inputs and checks if the `add` function is commutative or not. To check this property, we need some random integer pairs. This is where generators come in. The `Gen[A]` data type is used to generate random values of type `A`. ZIO Test provides numerous `Gen` instances for common types: ```scala val intGen: Gen[Any, Int] = Gen.int val stringGen: Gen[Sized, String] = Gen.string ``` It is also composable, so we can combine them to generate random values of more complex types: ```scala val stringIntGen: Gen[Sized, (String, Int)] = stringGen <*> intGen case class Person(name: String, age: Int) val personGen: Gen[Sized, Person] = stringIntGen.map(Person.tupled) ``` ZIO Test provides the `check` function for this purpose. It takes a list of generators and provides them to another taken function, which is a property checker: ```scala def property[T1, T2](input1: T1, input2: T2, ...): Boolean = ??? val input1Gen: Gen[_, T1] = ??? val input2Gen: Gen[_, T2] = ??? check(input1Gen, input2Gen, ...) { (input1, input2, ...) => assertTrue(property(input1, input2, ...)) } ``` In our example, the `is_add_commutative` predicate takes two inputs. So we need to pass two generators of type `Int` to the `check` function: ```scala def add(a: Int, b: Int): Int = ??? test("add is commutative") { check(Gen.int, Gen.int) { (a, b) => assertTrue(add(a, b) == add(b, a)) } } ``` ## Number of Samples In the previous example, we used `check` to test if the `add` function is commutative. In other words, we try to generate samples of random pairs of integers and try to falsify the `is_add_commutative` predicate. If we find a pair of integers that falsifies the predicate, then we know that the property is violated. By default, the `check` function, try to generate 200 samples. We can change this by using the `sample` test aspect: ```scala object AdditionSpec extends ZIOSpecDefault { def spec = test("add is commutative") { check(Gen.int, Gen.int) { (a, b) => assertTrue(add(a, b) == add(b, a)) } } @@ TestAspect.samples(10) } ``` To debug the test, we added a `println` statement inside the `check` function to see the generated samples. --- ## How Generators Work? A `Gen[R, A]` represents a generator of values of type `A`, which requires an environment `R`. The `Gen` data type is the base functionality for generating test data for property-based testing. We use them to produce deterministic and non-deterministic (PRNG) random values. It is encoded as a stream of optional samples: ```scala case class Gen[-R, +A](sample: ZStream[R, Nothing, Option[Sample[R, A]]]) ``` Before deep into the generators, let's see what property-based testing is and what problem it solves in the testing world. ## How Generators Work? We can think of `Gen[R, A]` as a `ZStream[R, Nothing, A]`. For example, the `Gen.int` is a stream of random integers `ZStream.fromZIO(Random.nextInt)`. To find out how a generator works, let's take a look at the following snippet. It shows how the `Gen` data type is implemented. :::caution Although it doesn't provide the exact implementation, this condensed edition of the "Gen" data type is sufficient to grasp how generators operate. For instance, we don't use a [pseudo-random generator](#random-generators-are-deterministic-by-default) throughout the following implementation. We haven't encoded the [shrinking algorithm](shrinking.md), either. ::: ```scala case class Gen[R, A](sample: ZStream[R, Nothing, A]) { def map[B](f: A => B): Gen[R, B] = Gen(sample.map(f)) def flatMap[R1 <: R, B](f: A => Gen[R1, B]): Gen[R1, B] = ??? def runCollect: ZIO[R, Nothing, List[A]] = sample.runCollect.map(_.toList) } object Gen { // A constant generator of the specified value. def const[A](a: => A): Gen[Any, A] = Gen(ZStream.succeed(a)) // A random generator of integers. def int: Gen[Any, Int] = Gen(ZStream.fromZIO(Random.nextInt)) def int(min: Int, max: Int): Gen[Any, Int] = ??? // A random generator of specified values. def elements[A](as: A*): Gen[Any, A] = if (as.isEmpty) Gen(ZStream.empty) else int(0, as.length - 1).map(as) // A constant generator of fixed values. def fromIterable[A](xs: Iterable[A]): Gen[Any, A] = Gen(ZStream.fromIterable(xs)) } Gen.const(42).runCollect.debug // Output: List(42) Gen.int.runCollect.debug // Output: List(82) or List(3423) or List(-352) or ... Gen.elements(1, 2, 3).runCollect.debug // Output: List(1) or List(2) or List(3) Gen.fromIterable(List(1, 2, 3)) // Output: List(1, 2, 3) ``` So we can see that the `Gen` data type is nothing more than a stream of random/constant values. ## Two Types of Generators We have two types of generators: 1. **Deterministic Generators**— Generators that produce constant fixed values, such as `Gen.empty`, `Gen.const(42)`, and `Gen.fromIterable(List(1, 2, 3))`. 2. **Random Generators**— Generators that produce random values, such as `Gen.boolean`, `Gen.int`, and `Gen.elements(1, 2, 3)`. ## Random Generators Are Deterministic by Default The important fact about random generators is that they produce deterministic values by default. This means that if we run the same random generator multiple times, it will always produce the same sequence of values to achieve reproducibility. So let's add some debugging print lines inside a test and see what values are produced: ```scala object ExampleSpec extends ZIOSpecDefault { def spec = test("example test") { check(Gen.int(0, 10)) { n => println(n) assertTrue(n + n == 2 * n) } } @@ samples(5) } ``` We can see, even though the `Gen.int` is a non-deterministic generator, every time we run the test, the generator will produce the same sequence of values: ```scala runSpec 9 3 0 9 6 + example test ``` This is due to the fact that the generator uses a pseudo-random number generator, which uses a deterministic algorithm. The generator provides a fixed seed number to its underlying deterministic algorithm to generate random numbers. As the seed number is fixed, the generator will always produce the same sequence of values. For more information, there is a separate page about this on [TestRandom](../services/random.md), which is the underlying service for generating test values. This behavior helps us to have reproducible tests. However, if we need non-deterministic test values, we can use the `TestAspect.nondeterministic` to change the default behavior: ```scala object ExampleSpec extends ZIOSpecDefault { def spec = test("example test") { check(Gen.int(0, 10)) { n => println(n) assertTrue(n + n == 2 * n) } } @@ samples(5) @@ nondeterministic } ``` ## How Samples Are Generated? When we run `check`, it creates an infinite stream by repeatedly sampling from the generator (using `forever`), then takes values from that stream—200 samples by default. We can override this using the `TestAspect.samples` test aspect, for example, use`@@ TestAspect.samples(5)` to take only 5 samples. Let's examine how samples are produced for each of the following generators: - `check(Gen.const(42))(n => ???)` will repeatedly sample from `ZStream.succeed(42)`, producing: 42, 42, 42, ... - `check(Gen.int)(n => ???)` will repeatedly sample from `ZStream.fromZIO(Random.nextInt)`, producing e.g.: 2, -3422, 33, 3991334, 98138, ... - `check(Gen.elements(1, 2, 3))(n => ???)` will repeatedly sample from `ZStream.fromZIO(Random.nextIntBounded(3).map(Chunk(1, 2, 3)(_)))`, producing e.g.: 3, 1, 1, 3, 2, ... - `check(Gen.fromIterable(List(1, 2, 3)))(n => ???)` will repeatedly sample from `ZStream.fromIterable(List(1, 2, 3))`, producing: 1, 2, 3, 1, 2, 3, ... When we run the `check` function with multiple generators, the samples will be the Cartesian product of their streams. Let's try some examples: ```scala test("two deterministic generators") { check(Gen.const(1), Gen.fromIterable(List("a", "b", "c"))) { (a, b) => println((a, b)) assertTrue(true) } } @@ TestAspect.samples(5) ``` The output will be: ```scala (1,a) (1,b) (1,c) (1,a) (1,b) + two deterministic generators 1 tests passed. 0 tests failed. 0 tests ignored. ``` So the example above is something like this: ```scala { for { a <- ZStream.succeed(1) b <- ZStream.fromIterable(List("a", "b", "c")) } yield (a, b) }.forever.take(5).runCollect.debug ``` Now let's try to use one non-deterministic generator and one deterministic generator: ```scala test("one non-deterministic generator and one deterministic generator") { check(Gen.int(1, 3), Gen.fromIterable(List("a", "b", "c"))) { (a, b) => println((a, b)) assertTrue(true) } } @@ TestAspect.samples(5) ``` Here is one example output: ```scala (3,a) (3,b) (3,c) (2,a) (2,b) + one non-deterministic generator and one deterministic generator 1 tests passed. 0 tests failed. 0 tests ignored. ``` This is the same as the previous example; it is like we have the following stream: ```scala { for { a <- ZStream.fromZIO(Random.nextIntBetween(1, 3)) b <- ZStream.fromIterable(List("a", "b", "c")) } yield (a, b) }.forever.take(5).runCollect.debug ``` ## Running a Generator for Debugging Purposes To run a generator, we can call the `runCollect` operation: ```scala val ints: ZIO[Any, Nothing, List[Int]] = Gen.int.runCollect.debug // Output: List(-2090696713) ``` This will return a `ZIO` effect containing all its values in a list, which in this example contains only one element. To create more samples, we can use `Gen#runCollectN`, which repeatedly runs the generator as much as we need. In this example, it will generate a list containing 5 integer elements: ```scala Gen.int.runCollectN(5).debug ``` In addition, there is an operator called `Gen#runHead`, which returns the first value generated by the generator. --- ## Introduction To Property Testing ## What is Property-Based Testing? In property-based testing, instead of testing individual values and making assertions on the results, we rely on testing the properties of the system which is under the test. To be more acquainted with property-based testing, let's look at how we can test a simple addition function. So assume we have a function `add` that adds two numbers: ```scala def add(a: Int, b: Int): Int = ??? ``` in a typical test we start with some well-known values as test inputs and check if the function returns the expected values for each of the pair inputs: | Input | Expected Output | |:---------:|:-----------------:| | (0, 0) | 0 | | (1, 0) | 1 | | (0, 1) | 1 | | (0, -1) | -1 | | (-1, 0) | -1 | | ... | ... | Now we can test all the inputs and make sure the `add` function returns the expected values: ```scala object AdditionSpec extends ZIOSpecDefault { def add(a: Int, b: Int): Int = ??? val testData = Seq( ((0, 0), 0), ((1, 0), 1), ((0, 1), 1), ((0, -1), -1), ((-1, 0), -1), ((1, 1), 2), ((1, -1), 0), ((-1, 1), 0) ) def spec = test("test add function") { assertTrue { testData.forall { case ((a, b), expected) => add(a, b) == expected } } } } ``` This is not a very good approach because it is very hard to find a set of inputs that will cover all possible behaviors of the addition function. Instead, in property-based testing, we extract the set of properties that our function must satisfy. So let's think about the `add` function and find out what properties it must satisfy: 1. **Commutative Property**— It says that changing the order of addends does not change the result. So for all `a` and `b`, `add(a, b)` must be equal to `add(a, b)`: ```scala assertTrue(add(a, b) == add(b, a)) ``` 2. **Associative Property**— This says that changing the grouping of addends does not change the result. So for all `a`, `b` and `c`, the `add(add(a, b), c)` must be equal to `add(a, add(b, c))`: ```scala assertTrue(add(add(a, b), c) == add(a, add(b, c))) ``` 3. **Identity Property**— For all `a`, `add(a, 0)` must be equal to `a`: ```scala assertTrue(add(a, 0) == a) ``` If we test all of these properties we can be sure that the `add` function works as expected, so let's see how we can do that using the `Gen` data type: ```scala object AdditionSpec extends ZIOSpecDefault { def add(a: Int, b: Int): Int = ??? def spec = suite("Add Spec")( test("add is commutative") { check(Gen.int, Gen.int) { (a, b) => assertTrue(add(a, b) == add(b, a)) } }, test("add is associative") { check(Gen.int, Gen.int, Gen.int) { (a, b, c) => assertTrue(add(add(a, b), c) == add(a, add(b, c))) } }, test("add is identitive") { check(Gen.int) { a => assertTrue(add(a, 0) == a) } } ) } ``` --- ## Operators 1. `Gen#zipWith` — Composes this generator with the specified generator to create a cartesian product of elements with the specified function: ```scala mdoc:compile-only Gen.elements("a", "b", "c").zipWith(Gen.elements("1", "2", "3"))(_ + _) .runCollectN(5) // Sample Output: List(b1, a2, c1, b1, b1) ``` 2. `Gen#zip` — Composes this generator with the specified generator to create a cartesian product of elements. ```scala mdoc:compile-only Gen.elements("a", "b", "c").zip(Gen.elements("1", "2", "3")) .runCollectN(5) (Gen.elements("a", "b", "c") <*> Gen.elements("1", "2", "3")) .runCollectN(5) // Sample Output: List((a,3), (a,3), (c,3), (b,3), (c,2)) ``` 3. `Gen#collect` — Maps the values produced by this generator with the specified partial function, discarding any values the partial function is not defined at: ```scala mdoc:compile-only Gen.int(-10, +10) .collect { case n if n % 2 == 0 => n } .runCollectN(5) .debug // Smaple Output: List(-6, -8, -2, 4, -6) ``` 4. `Gen#filter` — Filters the values produced by this generator, discarding any values that do not meet the specified predicate: ```scala mdoc:compile-only Gen.int(-10, +10).filter(_ % 2 == 0).runCollectN(5) // Sample Output: List(-6, 10, 0, -8, 4) ``` Using `filter` can reduce test performance, especially if many values must be discarded. It is recommended to use combinators such as `map` and `flatMap` to create generators of the desired values instead: ```scala mdoc:compile-only Gen.int(-10, +10).map(_ * 2).runCollectN(5) // Sample Output: List(2, 6, -6, 20, -14) ``` --- ## Shrinking In Property-Based Testing, we specify certain properties of a program, then we ask the testing framework to generate random test data to discover counterexamples. The existence of counterexamples shows that our function, which is under the test, is not correct. Unfortunately, in almost all cases, the first counterexample is not the minimal one, and they are fairly large or complex. So it is not a pretty good sample to describe why our test is failing. Shrinking is a mechanism that tries to find the smallest counterexample, which is the root cause of the test failure. So it helps a developer to find out why the test is failing. Finding the smallest failing case is somehow cumbersome and requires many attempts. As a developer, we do not need to do shrinking ourselves. All generators in ZIO Test have built-in shrinkers, so when we test properties, in case of test failures, the ZIO Test attempts to reduce the counterexamples forward their own zero points. Let's write a `reverse` function with an incorrect implementation: ```scala def reverse[T](list: List[T]): List[T] = if (list.length > 6) list.reverse.dropRight(1) else list.reverse ``` We know that if we reverse a list twice, it should give us the original list, so let's check this property: ```scala suite("ReverseSpec"){ // ∀ xs. reverse(reverse(xs)) == xs test("reversing a list twice must give the original list")( check(Gen.listOf(Gen.int)) { list => assertTrue(reverse(reverse(list)) == list) } ) } ``` The following messages, is a sample output of the test renderer, after running the test: ``` - ReverseSpec - reversing a list twice must give the original list Test failed after 7 iterations with input: List(0, 0, 0, 0, 0, 0, 0) Original input before shrinking was: List(724856966, 1976458409, -940069360, -191508820, -291932258, 1296893186, 2010410723, 1134770522, 1260002835) ✗ List(0, 0, 0, 0, 0, 0) was not equal to List(0, 0, 0, 0, 0, 0, 0) reverse(reverse(list)) == list reverse(reverse(list)) = List(0, 0, 0, 0, 0, 0) ``` The initial failing input discovered by ZIO Test is `List(724856966, 1976458409, -940069360, -191508820, -291932258, 1296893186, 2010410723, 1134770522, 1260002835)`. The ZIO Test then tries to find the simplest counterexample which is `List(0, 0, 0, 0, 0, 0, 0)`. So the property still fails with the final shrunk value. The original input is a list of 9 somewhat useless numbers, while after shrinking, we have a list of 7 zero numbers, so we can find the bug faster. --- ## Running Tests We can run ZIO Tests in two ways: 1. If we [added](installation.md) `zio.test.sbt.ZTestFramework` to SBT's `testFrameworks`, our tests should be automatically picked up by SBT on invocation of `test`: ```bash sbt test // run all tests sbt testOnly HelloWorldSpec // run a specific test ``` To run a specific test by their labels, we can use the `-t "