This part covers the Overview Document of ZIO and the video of A Tour of ZIO.

The ZIO Type

At the core of ZIO is ZIO: an effect type used to describe side effects in a simple, type-safe, testable and composable way.

The ZIO[-R, +E, +A] has three type parameters:

  • R: represents an environment of type R. If this type parameter is Any, it means the effect has no requirements because it can be an Unit value (). Example values are
    • Database connection, repository
    • Web Service, logging service
    • Application context, clock, random generator
    • Configuration, session data
  • E: a failure type representing a faile of type E. Often it is Throwable. If the type is Nothing, it means the effect cannot fail because there is no value of type Nothing. Example values are
    • Throwable
    • IOExceptions
    • DBError
    • MyAppError
    • Unit
  • A: a success type of A. If it is Unit, it means that the program is side-effect-only program that produces no useful information. If it is Nothing, it means the program runs forever or until it fails.

Because the ZIO[R, E, A] must have an A type, the R or E are optional. There are three possible aliases:

  • UIO[A]: is ZIO[Any, Nothing, A], an effect has no requirements and cannot fail.
  • URIO[R, A]: is ZIO[R, Nothing, A], an effect has requirments but cannot faile.
  • IO[E, A]: is ZIO[Any, E, A], an effect has no requirements.

There are two aliase if E is Throwable:

  • Task[A]: is ZIO[Any, Throwable, A], no requirements, can throw.
  • RIO[R, A]: is ZIO[R, Throwable, A], has requirements, can throw.

Therefore there are total six types: ZIO[R, E, A], UIO[A], URIO[R, A], IO[E, A], Task[A], and RIO[R, A] because R can be R or Any, and E can be E, Nothing or Throable. Each type defines methods to create values of the type.

Task is similar to Future that can throw or return data. UIO represents an infallible effects, including those resulting from handling all errors.

Creating Effects

  • ZIO.succeed method creates an succss effect. The method is eager, which means the parameter is evaluated before the method is invoked. The result is of type UIO[A].
  • ZIO.effectTotal is a lazy mehtod. The result is of type UIO[A].
  • ZIO.fail creates a failure effect of type IO[E, Nothing].
  • ZIO.fromOption creates a success effect of type IO[Unit, A] for Some[A], a failure effect of type IO[Unit, Nothing] for None.
  • ZIO.fromEither returns IO[Nothing, A] for Rigth[A], IO[E, Nothing] for Left[E].
  • ZIO.fromTry returns Task[A] for Try[A] because Try can only fail with values of type Throwable.
  • ZIO.fromFunction for function R => A returns a URIO[R, A].
  • ZIO.fromFuture returns Task[A] for Future[A].

A synchronous side-effect can be converted into a ZIO effect using ZIO.effect, for example, val getStrinLn: Task[String] = ZIO.effect(scala.io.StdIn.readLine()). Like Future, side-effect only throw exceptions. If a side-effect doesn’t throw any exception, use ZIO.effectTotal, for example def putStrLn(line: String): UIO[Unit] = ZIO.effectTotal(println(line)).

An asynchronous side-effect with a callback-based API can be converted into a ZIO effect using ZIO.effectAsync method. It returns a value of type IO[E, A] that has features such as interruption, resource-safety and good error-handling.

ZIO provides zio.blocking package for blocking IO. The effectBlock(Thread.sleep(10)) will be executed on a separate thread pool. Use effectBlockingCancelable for cancelable side-effects. The blocking method is used to run ZIO effect in the blocking thread pool.

Basic Operations

A ZIO effect provides many methods to process data or compose more effects.

  • map: transform success value. The shortcut for this is as.
  • mapError or mapErrorCause: transform failure value.
  • orElse: use an other effect when the first fails.
  • fold: handles both failure and success results.
  • catchAll or catchAllCause: handle all errors.
  • flatMap: the result of the first effect is the input of the second effect. When the first effect fails, the flatMap doesn’t run.
  • for expression: chain multiple effects using flatMap and map.
  • zip: the first executes first, then the second, the results are zipped into a tuple. If either fails, the cmposed effect fails.
  • zipRight and zipLeft: zipRight is *>, only keep the right. zipLeft is <*, only keep the left.

Handling Errors

ZIO provides full stack trace of errors. It gives the location of error, the next statement to be executed and many other useful informaiton.

You can surface failure with either that takes an ZIO[R, E, A] to ZIO[R, Nothing, Eiterh[E, A]]. The result is the same as URIO[R, Either[E, A]]. You can submerge failures with ZIO.absovle that transform an URIO[R, Either[E, A]] into ZIO[R, E, A].

Use catchAll to recover from all types of errors. Use catchSome with a partial function to recover from some types of errors. Both cannot rececude or eliminate the error type, they can wide the error type to a subtype. Use orElse to try another effect when the first fails.

The fold method lets you define non-effectful handler for failure and success. The foldM method lets your handle both in a effectful way.

The retry method teaks a Schedule and returns a new effect that will retry the first effect if it fails. retryOrElse allows both retry and, if all retries fail, try another effect. retryOrElseEither allows returning a differen type for the fallback.

Handling Resources

ZIO’s resource management provides guarantees in the presence of failure, interruption or defects in the application.

The ensuring(finalizer) guarantees that if an effect terminates for whatever reason, the finalizer will begin executing. The finalizer has a type of UIO[A] and is not allowed to fail. It must handle all errors internally. ensuring works across all types of effects, including asynchronous and concurrent effects.

The bracket method takes a release effect and a use effect. It guarantees to run to run the release effect, even in the presence of errors or interruption.

Basic Concurrency

ZIO provides concurrency via fibers. Fibers are low level. ZIO provides high-level operations built on fibers.

ZIO fibers consume almost no memory, have growable and shrinkable stacks, don’t waste resources blocking, and will be garbage collected automatically if they are suspended and unreachable.

Fibers are created and scheduled by ZIO runtime and cooperatively yield to each other. All effects are executed by some fibers. A fiber type Fiber[E, A] models an effect that is running. E is the failure type and A is the success value. A fiber represents a handle on the running computaiton.

The fork method creates a new fiber and execute the effect on this new fiber. The Fiber#join returns an effect.

Fiber#await returns an effect containing an Exit value that provides full information on how the fiber completed. Fiber#interrupt intterrupts the fiber and returns an Exit. by design, the effect returned by Fiber#interrupt does not resume until the fiber has completed. If this is not desired, you can fork the interruption as fiber.interrupt.fork.

Fiber#zip and Fiber#zipWith compose fibers. The Fiber.orElse runs the second fiber if the first fails.

ZIO provides parallel operations. These methods are named with a Par suffix. For example, zipPar, zipWithPar, collectAllPar, foreachPar, reduceAllPar, and mergeAllPar. If one fails, the others will be interrupted. If this is undesired, convert fallible effects into infallible effects using the ZIO#either or ZIO#option methods.

For first success, use fiber1 race fiber2. If the first success or failure, use fiber1.either race fiber2.either.

The ZIO#timeout method covnerts an effect into Option[A]. When it completes within the timeout, the result is Some[A], otherwise, None.

Running Effects

For a greenfield project, extends zio.APP and define run method def run(args: List[String]): ZIO[ZEnv, Nothing, Int]. The type ZEnv = Clock with Console with System with Random with Blocking is a resource that is provided by the default runtime: zio.Runtime.default. It can run effects that require any combination of these modules. The following is a simple application:

1
2
3
4
5
6
7
8
import zio._

object Main extends App {
  import zio.console._

  override def run(args: List[String]): ZIO[ZEnv, Nothing, Int] =
    putStrLn("Hello World!") as 0
}

An other way to use the default runtime is to use it to run logics directly without using zio.App. Foe example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import zio._

object Main {
  def main(args: Array[String]): Unit = {

    val logic = Task(println("Hi"))

    val runtime = Runtime.default
    runtime.unsafeRun(logic)
  }
}

A custom runtime Runtime[R] can be created with two values:

  • R: the Environment
  • Platform: to bootstrap the runtime system

The Platform has an error report that can be customized. The default is to log the error to standard error.

Ref and STM

Ref allows atomic get/set/update/modify. STM allows multiple operations and multiple Refs in a transaction. The TRef#collect mathod has built-in automatic retry. All operations are asynchronous and non-blocking.