This part covers the ZIO module pattern. ZIO uses a module pattern that a layer depends on the layers imediately below it without knowing their interal implementations. A module consists a set of methods that addresses one oncern. ZIO uses modules to create different application layers depending on each other and allows flexible composition for testing and changing.

1 The Basic Concepts

ZIO module is implemented using the following types:

1.1 Environment

The ZIO[R, E, A]: represents a type of an immutable value that lazily describes a functional effect (also called ZIO effect, or simply effect). A ZIO effect is a workflow or a job that require an environment of type R and may fail with an error of type E or succeed with a value of type A. An environment can be a generic type of R or a subtype of Has[_].

Environment values are provisioned via provide, provideSome, provideLayer, provideSomeLayer and provideCustomLayer methods.

  • ZIO.provide: ZIO object method defined as def provide[R, E, A](r: => R): ZIO[R, E, A] => IO[E, A] = (zio: ZIO[R, E, A]) => new ZIO.Provide(r, zio). It provides an R to to an effect ZIO[R, E, A to have another effect without R, i.e., a type of IO[E, A].
  • ZIO#provide: ZIO trait method defined as def provide(r: R)(implicit ev: NeedsEnv[R]): IO[E, A] = ZIO.provide(r)(self). Eliminate the dependency on R.
  • ZIO#provideSome: ZIO trait method defined as final def provideSome[R0](f: R0 => R)(implicit ev: NeedsEnv[R]): ZIO[R0, E, A] = ZIO.accessM(r0 => self.provide(f(r0))). It provides some of the environment required to run this effect, leaving the remainder R0.
  • ZIO#provideLayer: providers a layer to the ZIO effect to translate it to another layer.
  • ZIO#provideSomeLayer[R0 <: Has[_]](layer: ZLayer[R0, E1, R1]): ZIO[R0, E1, A]: the environment has two parts, provide one part and leaves the reaminder R0.
  • ZIO#provideCustomLayer: defined as final def provideCustomLayer[E1 >: E, R1 <: Has[_]](layer: ZLayer[ZEnv, E1, R1])(implicit ev: ZEnv with R1 <:< R, tagged: Tag[R1]): ZIO[ZEnv, E1, A] = provideSomeLayer[ZEnv](layer). It provides an effect with required environment that is not part of the ZEnv, leaving the effect that only depends on the ZEnv.

Access the environment value using ZIO.access[R](f) or ZIO.accessM[R](f) to perform an opeartion f using R as input. The f can be pure (R => A) or effectful (R => ZIO(R, E, A)).

  • ZIO.access[R] creates an AccessPartiallyApplied[R] - a class that defines def apply[A](f: R => A): URIO[R, A] = new ZIO.Read(r => succeedNow(f(r))).
  • ZIO.accessM[R] create an AccessMPartiallyApplied[R] - a class that defines def apply(f: R => ZIO[R, E, A]) = new ZIO.Read(f).

1.2 Has

The trait Has[A] is used with ZIO environment to express an effect’s dependency on a service of type A. For example, RIO[Has[Console.Service], Unit] is an effect depedning on Console.Service. It is a pattern to use a type alias for service such as type Console = Has[Console.Service]. Therefore it is hard to tell if an environment has a type of Has[_].

Has[M.Service]: represents an environment of typeM.Service, theHas[_]is theRinZIO[R, E, A]. Usually it is a collection of methods defined in atrait Servicein a module objectM. MultipleHas[A]` can be combined.

1.3 ZLayer

A ZLayer[-RIn, +E, ROut <: Has[_]] is an environment of type Rout. It represents an implementation of a Has[_] type. It describes a layer of an application (onion archtiecture). A layer describes a module’s dependencies and output: a module depends on some servcies (the input RIn) and produces some services (the output ROut is a subtype of Has[_]). Construction of a layer may be effectful.

It utitlizes resources (ZManaged) to manage resources because some resources must acquired and safely released. Layers can be composed horizonally or vertically. ZLayer is used to build the dependency hierarchy in a ZIO application. A ZLayer can be shared, i.e., only one instance is created, or created individually.

In short:

  • R is often represented as Has[A] to make it composable.
  • ZLayer represents an implementation of a module. It uses ZManaged to wrap input R and output Has[A]. ZLayer can be composed both horizontally and vertically.
  • Use ZIO.accessM[Has[A]]. (for effectful operation) or ZIO.access[Has[A]] (for pure operation) to access Has[A] and then call get to get an instance of A and use methods defined in A.
  • To inject dependency, use ZIO.provideLayer and related methods to provide R or ZLayer.

2 The Module Pattern

A module is an object that defines a set of methods in its Service trait. The service methods are represented by a Has[_] data type and it becomes the output type of an ZLayer value. The module object may define one or more ZLayer implementations of the interface.

ZIO uses ZLayer to deal with the R, the environment parameter, of a ZIO type. A ZIO effect uses the ZIO#provide(r: R) method of an effect to provide a runtime environment for the effect. It eleminates the environment dependency in the resulting effect type, i.e., a value of ZIO[R, E, A] becomes a value of IO[E, A]. The result effect type could also beUIO[A] (no error) or Task[A] (throwable error). The Runtime#unsafeRun method takes an IO[E, A] value. The Runtime#unsafeRunTask method takes a Task[Throwable, A] value.

Building a module has the following steps:

  • Define an object that gives the name to the module, such as object ModuleName {... }. Define the following fields in the object. Define the trait Service and type alais inside the object.
  • Define a type alias type ModualeName = Has[Service]. The type represents a collection of methods defined in the service.
  • Define a trait Service { def method1...} that defines the interface methods.
  • Define each service method as def method1(...): ZIO[R, E, ModuleName] = ZIO.accessM(_.get.method1(...)), the R and E are the method’s input environment type and error type. This allows one to use the service method directly from the obbject.
  • Define the different implementations of ModuleName through different methods of ZLayer. Use ZLayer.fromFunction to specify input environment.

Following is the commented implementation of zio.system.System:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package zio

import java.lang.{ System => JSystem }

package object system {

  type System = Has[System.Service]

  object System extends Serializable {
    trait Service extends Serializable {
      def env(variable: String): IO[SecurityException, Option[String]]

      def property(prop: String): IO[Throwable, Option[String]]

      def lineSeparator: UIO[String]
    }

    val live: Layer[Nothing, System] = ZLayer.succeed(
      new Service {

      def env(variable: String): IO[SecurityException, Option[String]] =
        IO.effect(Option(JSystem.getenv(variable))).refineToOrDie[SecurityException]

      def property(prop: String): IO[Throwable, Option[String]] =
        IO.effect(Option(JSystem.getProperty(prop)))

      val lineSeparator: UIO[String] = IO.effectTotal(JSystem.lineSeparator)
    })
  }

  /** Retrieve the value of an environment variable **/
  def env(variable: => String): ZIO[System, SecurityException, Option[String]] =
    ZIO.accessM(_.get env variable)

  /** Retrieve the value of a system property **/
  def property(prop: => String): ZIO[System, Throwable, Option[String]] =
    ZIO.accessM(_.get property prop)

  /** System-specific line separator **/
  val lineSeparator: ZIO[System, Nothing, String] =
    ZIO.accessM(_.get.lineSeparator)
}

3 The Has Date Type

The Has[A] type represents a dependency on a service of type A. Has collects its services in a map. Its get[ModuleName] method accesses the service imlementation. Two Has[_] values can be combined horizontally through + (for a new service) and ++ (for another Has[B]) operators. For example:

1
2
3
4
5
6
7
8
val repo: Has[Repo.Service] = Has(new Repo.Service{})
val logger: Has[Logger.Service] = Has(new Logger.Service{})

val mix: Has[Repo.Service] with Has[Logger.Service] = repo ++ logger

// get back a service and use its methods from the mixed value:
mix.get[Logger.Service].log("Hello modules!")
mix.get[Repo.Service].getUser(myUserId)

The power of Has is that it can collect and provide all service methods from one place. Below is partial code from the ZEnv.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type ZEnv = Clock with Console with System with Random with Blocking

object ZEnv {
  private[zio] object Services {  // don't know why define it here and below
    val live: ZEnv =
      Has.allOf[Clock.Service, Console.Service, System.Service, Random.Service, Blocking.Service](
        Clock.Service.live,
        Console.Service.live,
        System.Service.live,
        Random.Service.live,
        Blocking.Service.live
      )
}

val live: Layer[Nothing, ZEnv] =
  Clock.live ++ Console.live ++ System.live ++ Random.live ++ Blocking.live

4 The ZLayer Data Type

ZIO uses the ZLayer[-RIn, +E, + ROut <: Has[_]] to build an enviornment of Has[], from a value RIn with a possible error type of E during creation. Layer[E, R] is an aliase of ZLayer[Any, E, R]. There are several ways to create a ZLayer:

There are many ways to create a ZLayer:

  • ZLayer.succeed or ZIO.asService: to create a layer from an existing service.
  • ZLayer.succeedMany: to create a layer from a value that is one or more services.
  • ZLayer.fromFunction: to create a lyaer from a fuinction from an enviornment to the service.
  • ZLayer.fromEffect: to left a ZIO effect to a layer requiring the effect enviornment.
  • ZLayer.fromAquireRelease: create a layer based on resource acquisition/release.
  • ZLayer.fromService: create a layer depends on a specified service.
  • ZLayer.fromServices: create a layer depends on the specified services.

There are some variants of creation with different suffixes: build a service effectfully with a suffix of M, resourcefully with a suffix of Managed, or combinately with a suffix of Many.

The ++ composes layers horizontally that includes the requirements of both layers. To build a layer vertically, i.e., one layer is used as the input to another, use layerA >>> layerB, the composed layer has a requirement of the first and the output of the second layer. The first layer’s output is the input of the second layer.

Be default, ZIO layers are acquired in parallel and are shared. a non-shared version can be created by ZLayer.fresh.

5 An Example

The following defines a module interface and one implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import zio._

case class UserId(id: Long)
case class User(id: UserId, name: String)

object UserRepo {

  type UserRepo = Has[UserRepo.Service]

  trait Service {
    def getUser(userId: UserId): IO[Throwable, Option[User]]
    def createUser(user: User): IO[Throwable, Unit]
  }

  def getUser(userId: UserId): ZIO[UserRepo, Throwable, Option[User]] =
    ZIO.accessM(_.get.getUser(userId))

  def createUser(user: User): ZIO[UserRepo, Throwable, Unit] =
    ZIO.accessM(_.get.createUser(user))

  val inMemory: Layer[Nothing, UserRepo] = ZLayer.succeed(
    new Service {
      def getUser(userId: UserId): IO[Throwable, Option[User]] = UIO {
        if (userId.id == 7) {
          Some(User(userId, "in memory name"))
        } else None
      }
      def createUser(user: User): IO[Throwable, Unit] = UIO { () }
    }
  )
}

The following module depends on another module:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import zio._

type Logging = Has[Logging.Service]

object Logging {
  trait Service {
    def info(s: String): UIO[Unit]
    def error(s: String): UIO[Unit]
  }

  def info(s: String): URIO[Logging, Unit] = ZIO.accessM(_.get.info(s))
  def errorr(s: String): URIO[Logging, Unit] = ZIO.accessM(_.get.error(s))

  import zio.console.Console
  val consoleLogger: ZLayer[Console, Nothing, Logging] =
    ZLayer.fromFunction(console =>
      new Service {
        def info(s: String): UIO[Unit] = console.get.putStrLn(s"info- $s")
        def error(s: String): UIO[Unit] = console.get.putStrLn(s"error- $s")
      }
    )
}

A sample application uses the above modules:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import zio._
import zio.console.Console
import UserRepo._
import Logging._
import zio.clock.Clock
import zio.random.Random

object Main extends App {
  override def run(args: List[String]) =
    program2 // program 1 runs another example

  val user2 = User(UserId(7), "Tommy")

  val makeUser: ZIO[Logging with UserRepo, Throwable, Int] = for {
    _ <- info(s"insert user: $user2")
    _ <- createUser(user2)
    - <- info("user created")
  } yield 0

  val horizontal: ZLayer[Console, Nothing, Logging with UserRepo] =
    consoleLogger ++ inMemory
  val fullLayer: Layer[Nothing, Logging with UserRepo] =
    Console.live >>> horizontal

  // a program with all dependencies provided
  val program = makeUser.provideLayer(fullLayer).catchAll(_ => ZIO.succeed(1))

  val makeUser2
      : ZIO[Logging with UserRepo with Clock with Random, Throwable, Int] =
    for {
      uId <- zio.random.nextLong.map(UserId)
      createdAt <- zio.clock.currentDateTime.orDie
      _ <- Logging.info(s"inserting user")
      _ <- UserRepo.createUser(User(uId, "Chet"))
      _ <- Logging.info(s"user inserted, created at $createdAt")
    } yield 0

  // another program with additional dependencies provided by ZEnv
  val program2: ZIO[ZEnv, Nothing, Int] =
    makeUser2.provideCustomLayer(fullLayer).catchAll(_ => ZIO.succeed(1))
}

6 Summary

The ZLayer has the following patterns:

  • Ogranize services into a module that is defined by an object.
  • Defining the service interface, the type aliase and access methods is almost the same for each module.
  • Each service implementation declares its dependencies.
  • Modules can be composed vertically and horizontally, in different places. For example, system-wide dependencies can be provided by a custom runtime.