This part covers the Modules and Layers.

ZLayer

ZIO uses the provide method of an effect to provide a runtime environment for the effect. It eleminates the environment dependency in the resulting effect type, i.e., represented by type Any of the resulting enviornment. The result effect type could be UIO[A], IO[E, A], or Task[A],

ZIO uses a moudle pattern that a layer depends on the layers imediately below it without knowing their interal implementations. A module is a group of functions addressing a single concern. ZIO suggests the following steps to create a module:

  • Define an object that gives the name to the module, such as object ModuleName.
  • Within the module object define a trait Service { ...} that defines the interface methods.
  • Within the module object define the different implementations of ModuleName through ZLayer.
  • Define a type alias type ModualeName = Has[ModuleName.Service].

The Has[A] represents a dependency on a service of type A. Two Has[] can be combined horizontally through + and ++ operators. For example:

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

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

val log = mix.get[Logger.Service].log("Log Service")

Has collects its services in a map. The get[ModuleName] method accesses the service imlementation. Usually use ZLayer to create a Has. The ZLayer[-RIn, +E, + ROut <: Has[_]] is used to build an enviornment of type ROut, 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:

  • ZLayer.succeed or ZIO.asService to create a layer from an existing service.
  • ZLayer.fromFunction to create a layer from a function from the requirment to the service.
  • ZLayer.fromEffect to life a ZIO effect to a layer requiring the effect enviornment.
  • ZLayer.fromAcquireRelease to create a layer based on resource acquisition/release, like ZManaged.
  • ZLayer.fromService to build a layer from a number of required 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 ZEnv type includes built-in services such as Clock, Console, Random, System, Blocking. These can be provided simialr to val zEnvApp: ZIO[ZEnv, E, A] = app.provideCustomLayer(fullLayer), the runtime will provide the services.

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

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

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

  type Logging = Has[Logging.Service]

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

Summary

The ZLayer has the following patterns:

  • Define the service interface, the type aliase and access methods are 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.