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.

0 Common Patterns

There are three common ways to create a ZLayer, the difference is the input type. If the input type is Has[_], use fromFunction. If the input type is the actual service trait, use fromService. Using fromServices to build a layer from multiple services.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def fromFunction[A, B: Tag](f: A => B): ZLayer[A, Nothing, Has[B]] =
    fromFunctionM(a => ZIO.succeedNow(f(a)))

def fromService[A: Tag, B: Tag](f: A => B): ZLayer[Has[A], Nothing, Has[B]] =
    fromServiceM[A, Any, Nothing, B](a => ZIO.succeedNow(f(a)))

// when call this, specify the type names
def fromServices[A0: Tag, A1: Tag, B: Tag](
    f: (A0, A1) => B
  ): ZLayer[Has[A0] with Has[A1], Nothing, Has[B]] = {
    val layer = fromServicesM(andThen(f)(ZIO.succeedNow(_)))
    layer
  }

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 as a 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 type M.Service, the Has[_] is the R in ZIO[R, E, A]. Usually it is a collection of methods defined in atrait Service or a class Service in a package object or module object M. MultipleHas[A] can be combined.

1.3 ZLayer

A ZLayer[-RIn, +E, ROut <: Has[_]] is a recipe to create an environment of type Rout. It represents an dependecy relationship 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.

Thre are three ways to build a module.

2.1 Simple Module

This kind of module is used as a dependency of another module and it is fixed with one implementation – not good for testing. For simple scenarios or business logic module (not changed, only replaced), testing may not be an issue. There are two steps to create a simple module:

  • define a class with its constructor depending on a service
  • define a companion object to create a ZLayer.

The following is an example:

1
2
3
4
5
6
7
8
9
class ModuleName(dep: DependentService) {
  def method1 = ???
  def method2 = ???
}

object ModuleName {
  // pay attention to the ZLayer first parameter, the type is Has[DependentService]
  val live: ZLayer[Has[DependentService], E, Has[ModuleName]] = ZLayer.fromService(new ModuleName(_))
}

2.2 Module in ZIO docs

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 an access method for each service method as def method1(...): ZIO[ModuleName, E, ModuleName] = ZIO.accessM(_.get.method1(...)), the R and E are the method’s input environment type and error type. This allows the business domain logic to use the service methods directly from the object ModuleName.
  • Define the different implementations of ModuleName.Service. It is good idea to define the implementation in a separate file using a class whose constructor uses the dependent service. For example: class implementOne(dep: DepService) extends ModuleName.Service {...}.
  • Defeine one or more ZLayer using the above implementation. Use ZLayer.fromFunction or ZLayer.fromService to specify dependeent environment. ZLayer.fromFunction takes an input of type R that is also the environment type of the resulted ZLayer. User r.get to get the service is R is Has[_]. The ZLayer.fromService take an input of type Service and the enviornemtn type of resulted ZLayer is Has[Service].

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
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]]
    }

    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)))
    })
  }

  /** 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)
}

This method use a trait, an object to define a module, not defining an aliase for Has[Service]. In my opinion, this is the recommended styple because it uses consistent name for the trait and object. Also the use of Has making it clear that the type is a Has[_] type.

 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
import java.lang.{ System => JSystem }

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

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

object System {
  val live: Layer[Nothing, Has[System]] = ZLayer.succeed(
    new System {
    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)))
  })

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

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

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 R to the service. R could be an environment or a Has[_]. If it is a Has[_], call R.get to access the service. The result’s enviornment is still the orginal R.
  • 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. The input is a Service and the result’s enviornment is Has[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. It hides the LayerB. To expose both enviornments, use layerA >+> layerB.

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
42
import zio._
import zio.console.Console
import zio.clock.Clock
import zio.random.Random

import UserRepo._
import Logging._

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.