This part is based on the Overview Document of ZIO and the video of A Tour of ZIO. ZIO is a library for asynchronous and concurrent programming based on pure functional programming model. It is primarily not an IO effect library!

1 The ZIO Type

A key concept in ZIO is that you describe the values and functions using the type ZIO[R, E, A]. A value of ZIO[R, E, A] is an immutable data structure representing a function of type R => Either[E, A]. It is not just the low level functions, the whole program is also represented by such a value.

Once you use the ZIO type, you stay with it and use its methods to map, chain, etc to work with it. Then the feature is completely described, you use prodive method to provide the R and make it a runnable type of IO. Finally you run it with runtime.unsafeRun(runable).

1.1 The Basci ZIO Type

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

  • R: represents a requirement of 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.

1.2 Aliases

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. To help remember, you can think that it represents an Unit IO that turns a unit value into an effect. A unit value cannot fail.
  • URIO[R, A]: is ZIO[R, Nothing, A], an effect has requirments but cannot fail. It wraps a function that takes an environment R and produces a value of type A. The wrapped function is a total function that cannot fail.
  • IO[E, A]: is ZIO[Any, E, A], an effect that doesn’t depend on any environment.

There are two aliase if E is Throwable:

  • Task[A]: is IO[Throwable, A], a special subtype of IO[E, A] that can throw.
  • RIO[R, A]: is ZIO[R, Throwable, A], a subtype of ZIO[R, E, A] can throw error.

Therefore there are total six types: ZIO[R, E, A], IO[E, A], RIO[R, A], UIO[A], Task[A], and URIO[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.

The relationship is:

ZIO –> IO –> Task–> UIO ZIO –> RIO –> URIO –> UIO

1.3 Examples

Some examples from the ZIO document:

1
2
3
4
5
6
7
8
val value: UIO[String] = IO.succeed("Hello World")
val effectTotalTask: Task[Long] = IO.effectTotal(System.nanoTime())
def readFile(name: String): IO[IOException, Array[Byte]] =
  IO.effect(FileUtils.readFileToByteArray(new File(name))).refineToOrDie[IOException]

// assume that the `Http.req` is an async call
def makeRequest(req: Request): IO[HttpException, Response] =
  IO.effectAsync[HttpException, Response](k => Http.req(req, k))

2 Creating Effects

The blog Wrapping impure code with ZIO has some examples.

2.1 Creating IO[E, A]

ZIO methods provide, done, fromEither, fromFiber and fromFiberM to produce an IO[E, A]. These methods either provide a R or transform SomeType[E, A] to IO[E, A].

2.2 Creating RIO[R, A]

ZIO methods effectSuspend, effectSuspendWith, and fromFunctionFuture produce a RIO[R, A].

2.3 Creating UIO[A]

There are three ways to create a UIO[A] effect in ZIO:

  • ZIO.effectTotal: def effectTotal[A](effect: => A): UIO[A] = new ZIO.EffectTotal(() => effect)
  • ZIO.succeed: def succeed[A](a: => A): UIO[A] = effectTotal(a)
  • ZIO.some: def some[A](a: => A): UIO[Option[A]] = succeed(Some(a))

In addition to UIO.apply method, IO, RIO, UIO, URIO and Task all have the effectTotal, succeed and some methods.

2.4 Creating Task[A]

  • ZIO methods: apply, effect, fromFuture, fromFutureInterrupt, fromTry7 and getOrFail.
  • Task methods

2.5 Creating URIO[R, A]

  • ZIO methods: either, ensuring, eventually, fold, foldCause, orDie, orDieWith, fromFunction, environment, ignore, merge, orElseSucceed, option, fork, forkAs, forkDaemon, forkAll, forkWithErrorHandler, forkAll_, raceAll, to, toFuture, toFutureWith, collectAllSuccesses, collectAllSuccessesPar, collectAllSuccessesParN, environment, fromFunction, runTime,
  • URIO methods

2.6 Creating ZIO[R, E, A]

There are dozens of ZIO methods that create ZIO[R, E, A]. That’s the most common result of a ZIO operation.

2.7 From Values

  • Success values: ZIO.succeed, ZIO.effectTotal, ZIO.effect etc
  • Failure values: ZIO.fail, Task.fail
  • Scala values: ZIO.fromOption, ZIO.fromEither, ZIO.fromTry
  • Function values: ZIO.fromFunction
  • Future: ZIO.fromFuture

2.8 Sync and Async

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 effectAsync, effectAsyncInterrupt, effectAsyncM, and effectAsyncMabye methods of ZIO. It returns a value of type ZIO[R, E, A] that has features such as interruption, resource-safety and good error-handling. Here is an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
object legacy {
  def login(
    onSuccess: User => Unit,
    onFailure: AuthError => Unit): Unit = ???
  )
}

val login: IO[AuthError, User] =
  IO.effectAsync[AuthError, User] { callback =>
    legacy.login(
      user => callback(IO.succeed(user)),
      err => callback(IO.fail(err))
    )
  }

2.9 Blocking Code

ZIO provides zio.blocking package for blocking IO such as file or network calls. 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 that may increase to a big number of threads.

3 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.

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

4 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.

5 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.

6 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 the result IO[E, A].

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.

An Exit[E, A] describes the result of executing an IO value. The result is either succeeded with a value A, or failed with a Cause[E].

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.

7 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 following code creates a Runtime that provides an Int to effects:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import zio.{ZIO, Runtime}

object MyApp {
  def main(args: Array[String]): Unit = {
    import zio.internal.Platform

    val myRuntime: Runtime[Int] = Runtime(42, Platform.default)

    val p = (x: Int) => println(s"Hi: ${x}")
    val effect = ZIO.fromFunction[Int, Unit]((x: Int) => println(s"Hi: ${x}"))
    myRuntime.unsafeRun(effect)
  }
}

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