How to use ZLayer in ZIO

men working on a computer
Reading Time: 5 minutes

What is ZIO:

ZIO is a library for Scala programming language that provides a pure, composable, and type-safe approach to error handling and asynchronous programming. ZIO provides a lot of tools for developers to write applications in a clean, concise, and functional manner. Zlayer is a module in ZIO that provides abstractions for building and composing modular applications.

In this blog, we’ll explore the basics of Zlayer and how to use it in ZIO.

What is Zlayer?

Zlayer is a module in ZIO that provides an easy and composable way to manage dependencies in your application. It allows you to abstract the dependencies of a particular module, making it easier to test and swap them out if needed.

In comparison to other languages, Zlayer allows for better separation of concerns and improved modularity. It provides a mechanism for composing and abstracting functionality into reusable components. Zlayer makes it easier to manage the dependencies of an application. It provides a way to specify and manage the dependencies required by a component. This can result in improved maintainability and testability of an application.

ZLayer[-RIn, +E, +ROut] describes a layer of an application: every layer in an application requires some services as input RIn and produces some services as the output ROut.

There are many ways to create a ZLayer. Some of them are:

  • ZLayer.succeed to create a layer from an existing service
  • ZLayer.succeedMany to create a layer from a value that’s one or more services
  • ZLayer.fromFunction to create a layer from a function from the requirement to the service
  • ZLayer.fromEffect to lift a ZIO effect to a layer requiring the effect environment
  • ZLayer.fromAcquireRelease for a layer based on resource acquisition/release. The idea is the same as ZManaged.
  • ZLayer.fromService to build a layer from a service
  • ZLayer.fromServices to build a layer from a number of required services
  • ZLayer.identity to express the requirement for a layer
  • ZIO#toLayer or ZManaged#toLayer to construct a layer from an effect

Let’s discuss some of them:

ZLayer.succeed:

ZLayer.succeed is a constructor in the ZLayer companion object in ZIO that creates a new ZLayer by taking a constant value as an argument and wrapping it into a Task with a successful outcome. This value represents the environment that the ZLayer provides.

Here’s a simple example:

val liveService = new Service { ... }

val liveServiceLayer = ZLayer.succeed(liveService

In this example, the liveService instance is wrapped in a Task with a successful outcome using ZLayer.succeed, and the resulting ZLayer provides the liveService as the environment. This ZLayer can then be used in a provideLayer call to provide the environment for a ZIO program.

ZLayer.succeedMany:

ZLayer.succeedMany is a constructor in the ZLayer companion object in ZIO that creates a new ZLayer by taking a list of values as an argument and wrapping each value into a Task with a successful outcome. The values represent the environment that the ZLayer provides.

Here’s a simple example:

val liveService = new Service { ... }

val liveConfig = new Config { ... }

val liveServiceLayer = ZLayer.succeedMany(liveService, liveConfig)

In this example, the liveService and liveConfig instances are both wrapped in Tasks with successful outcomes using ZLayer.succeedMany, and the resulting ZLayer provides both liveService and liveConfig as the environment. This ZLayer can then be used in a provideLayer call to provide the environment for a ZIO program.

ZLayer.fromEffect:

ZLayer.fromEffect is a constructor in the ZLayer companion object in ZIO that creates a new ZLayer by taking an effect as an argument and wrapping it into a Task. The effect represents the environment that the ZLayer provides.

Here’s a simple example:

val liveService: Task[Service] = Task { new Service { ... } }

val liveServiceLayer = ZLayer.fromEffect(liveService)

In this example, the liveService effect is wrapped into a Task using ZLayer.fromEffect, and the resulting ZLayer provides the liveService as the environment. This ZLayer can then be used in a provideLayer call to provide the environment for a ZIO program.

ZLayer.identity:

ZLayer.identity is a constructor in the ZLayer companion object in ZIO that creates a new ZLayer that does not modify the environment and just passes it through.

Here’s a simple example:

val liveService = new Service { ... }

val liveServiceLayer = ZLayer.succeed(liveService)

val identityLayer = ZLayer.identity[Service]

val program = ZIO.access[Service](_.get)

In this example, the liveService instance is wrapped in a Task with a successful outcome using ZLayer.succeed, and the resulting ZLayer provides the liveService as the environment. The identityLayer ZLayer is created using ZLayer.identity[Service], which creates a ZLayer that does not modify the environment but just passes it through. The program uses ZIO.access to access the environment and retrieve the Service instance.

Let’s understand this with a simple example:

import zio._

// trait that defines an interface for a greeting service

trait Service {

  def greet(name: String): UIO[String]
}

class LiveService extends Service {

  def greet(name: String): UIO[String] = UIO(s"Hello, $name")
}

// ZLayer that succeeds with an instance of LiveService

val liveServiceLayer = ZLayer.succeed(new LiveService)

//program accessing instance of the Service trait

val program: ZIO[Service, Nothing, String] =

  for {

    service <- ZIO.access[Service](_.get)

    greeting <- service.greet("Ram")

  } yield greeting

val runtime = Runtime.default

val result = runtime.unsafeRun(program.provideLayer(liveServiceLayer))

println(result) // Output: Hello, Ram

In this example, Service is a trait that defines an interface for a greeting service. LiveService is an implementation of that interface that provides a concrete implementation of the greet method.

We then define a ZLayer called liveServiceLayer that succeeds with an instance of LiveService.

Finally, we define a ZIO program program that accesses an instance of the Service trait, and uses that instance to greet someone named “Ram”.

The program is executed by passing the liveServiceLayer to the provideLayer method, which provides the required environment for the program to run. The result is then obtained by running the program using the unsafeRun method of the Runtime.

Here is one more example which can be more helpful in understanding ZLayer:

import zio._

trait Database {

  def fetchData(id: Int): UIO[String]

}

class LiveDatabase extends Database {

  def fetchData(id: Int): UIO[String] = UIO(s"Data for id $id")

}

trait Logger {

  def log(message: String): UIO[Unit]

}

class ConsoleLogger extends Logger {

  def log(message: String): UIO[Unit] = UIO(println(message))

}

val liveDatabaseLayer = ZLayer.succeed(new LiveDatabase)

val consoleLoggerLayer = ZLayer.succeed(new ConsoleLogger)

val program: ZIO[Database with Logger, Nothing, String] =

  for {

    db <- ZIO.access[Database](_.get)

    _ <- ZIO.access[Logger](_.get).flatMap(_.log("Fetching data from the 

database"))

    data <- db.fetchData(1)

  } yield data

val runtime = Runtime.default

val result = runtime.unsafeRun(program.provideSomeLayer[Console with 

Database](consoleLoggerLayer ++ liveDatabaseLayer))

println(result) // Output: Data for id 1

In this example, we define two different traits, Database and Logger. We then provide two implementations of those traits, LiveDatabase and ConsoleLogger, respectively.

We create two ZLayers, liveDatabaseLayer and consoleLoggerLayer, which provide instances of LiveDatabase and ConsoleLogger, respectively.

Finally, we define a program program that uses both the Database and Logger traits. The program logs a message first indicating that it is fetching data from the database. Then it finally returns the data.

The program is executed by providing the required environment via provideSomeLayer, and passing in the combined ZLayer of consoleLoggerLayer and liveDatabaseLayer. The result is then obtained by running the program using the unsafeRun method of the Runtime.

Conclusion:

In conclusion, ZLayer is a powerful concept in ZIO that enables the modularization of environment and dependencies in ZIO programs. ZLayer can be composed and combined to create complex environments and provide them to ZIO programs using provideLayer. The various constructors available in the ZLayer companion object, such as ZLayer.succeed, ZLayer.succeedMany, ZLayer.fromEffect, and ZLayer.identity, provide a flexible and convenient way to create ZLayer instances that can be used to represent different aspects of the environment and dependencies in ZIO programs. With the ability to modularize and manage environment and dependencies using ZLayer, ZIO developers can write more scalable and maintainable ZIO applications.

Written by 

Rituraj Khare is a Software Consultant at Knoldus Software LLP. An avid Scala programmer and Big Data engineer, he has experience with the tech stack such as - Scala| Spark| Kafka| Python| Unit testing| Git| Jenkins| Grafana.