
Introduction
According to ZIO documentation, we know ZIO is a library for asynchronous and concurrent programming that promotes pure functional programming.
The core of the ZIO library provides a powerful effect type ZIO inspired by Haskell’s IO monad. The value of this type doesn’t do anything itself. It is just a description of something that should be done.ZIO runtime system is responsible for actually doing what is described.
ZIO[-R, +E, +A]
datatype has 3 type parameters:
- R – type of environment required to interpret the effect
- E – error type
- A – return type
This data type can also be expressed in the form of R => Either[E, A]. Getting more information about ZIO type refers to the official documentation.
ZIO allows us to define modules and use them to create different application layers relying on each other. A module is a group of functions that deals with only one concern.
ZIO follows the Module Pattern in which the layer depends upon the interfaces exposed by the layers immediately below itself. This means each layer depends on the layers immediately below it, but it doesn’t know anything about their implementation details.
In this blog, we read about how we compose these layers using different ways effectively.
Library Dependency
Add the given dependency to your build.s
bt.
libraryDependencies += "dev.zio" %% "zio" % "1.0.12
ZLayer
The ZLayer data type is an immutable value that contains a description to build an
the environment of type ROut, starting from a value RIn, possibly producing an error E during
creation:
ZLayer[-RIn, +E, +ROut <: Has[_]]
The trait Has[A]
is used with the ZIO environment to express an effect’s dependency on a service of type A
.
For example,RIO[Has[Console.Service], Unit]
is an effect that requires a Console.Service
service.
We can create ZLayer from Simple values, Managed Resources, ZIO Effects, other services.
● ZLayer.succeed: Allows to create of a ZLayer from a Service. This is useful when you want to define a ZLayer whose creation doesn’t depend on anything and doesn’t fail.
● ZLayer.fail: This allows to build a ZLayer that always fails to build output.
● ZLayer.fromEffect: Allows to lift of a ZIO effect to a ZLayer. This is especially handy when you want to define a ZLayer whose creation depends on an environment and/or can fail. You can also use the equivalent operator in the ZIO data type: ZIO#toLayer.
● ZLayer.fromFunction: Allows to create of a ZLayer from a function whose input is an environment and whose output is a Service. You can use this when you want to define a ZLayer whose creation depends on an environment but can’t fail.
● ZLayer.fromManaged: Allows to lift a ZManaged effect to a ZLayer. This is applicable when you want to define a ZLayer whose creation depends on an environment and/or can fail, and when you want additional resource safety. You can also use the equivalent operator in the ZManaged data type: ZManaged#toLayer.
● ZLayer.fromAcquireRelease: This is very similar to ZLayer.fromManaged,but it expects a ZIO effect and a release function instead.
● ZLayer.fromService: Allows to create a ZLayer from a function whose input is a Service and whose output is another Service. This is useful when you want to define a ZLayer whose creation depends on another Service but can’t fail.
Let’s create two services Logger and Database. In this blog, we discuss only the how-to compose layers in ZIO for getting information about how we create layers in ZIO head on to this blog
Logger Service
object LoggerService {
type Logger = Has[Logger.Service]
object Logger {
trait Service {
def log(line: String): UIO[Unit]
}
val any: ZLayer[Logger, Nothing, Logger] =
ZLayer.requires[Logger]
val live: Layer[Nothing, Has[Service]] = ZLayer.succeed {
(line: String) => {
putStrLn(line).provideLayer(Console.live).orDie
}
}
}
def log(line: => String): ZIO[Logger, Throwable, Unit] =
ZIO.accessM(_.get.log(line))
}
Database Service
case class User(id: String,name: String)
object DatabaseService {
type Database = Has[Database.Service]
object Database {
trait Service {
def getUser(id: String): Task[User]
}
val any: ZLayer[Database, Nothing, Database] =
ZLayer.requires[Database]
val live: Layer[Nothing, Has[Service]] = ZLayer.succeed {
(id: String) => {
Task(User(id,"Akash"))
}
}
}
def getUser(id: => String): ZIO[Database, Throwable, User] =
ZIO.accessM(_.get.getUser(id))
}
The Logger Service prints the data onto the console and the Database service return the User. Now we use these Services to use other Service Users.
This Users Service depends on the DatabaseService and LoggerService to get and print user data.
Users Service
object UserRepo {
type Users = Has[Users.Service]
def getUser(id: => String): ZIO[Users, Throwable, Unit] =
ZIO.accessM(_.get.getUser(id))
object Users {
val any: ZLayer[Users, Nothing, Users] =
ZLayer.requires[Users]
val live: ZLayer[Has[Database.Service] with Has[Logger.Service], Nothing, Has[Service]] =
ZLayer.fromServices[Database.Service, Logger.Service, Service] { (database, logger) =>
new Service {
override def getUser(id: String): Task[Unit] = for {
user <- database.getUser(id)
_ <- logger.log(s"Hello $user")
} yield ()
}
}
trait Service {
def getUser(id: String): Task[Unit]
}
}
}
Layer Composition
Now first we know methods of composing layers.ZLayer
s can be composed together horizontally or vertically.
Horizontal Composition
They can be composed together horizontally with the ++
operator. When we compose two layers horizontally, the new layer that this layer requires all the services that both of them require, also this layer produces all services that both of them produce.
Horizontal composition is a way of composing two layers side-by-side. It is useful when we combine two layers that don’t have any relationship with each other.
In other words, if we have two layers ZLayer[RIn1, E1, ROut1] and ZLayer[RIn1, E1, ROut1] then we can obtain a bigger ZLayer
which can take as input RIn1 with RIn2
, and produce as output ROut1 with ROut2
.
In our use case, we can combine Database and Logger horizontally, because they have no dependencies and can produce a powerful layer that combines Has[Database.Service] with Has[Logger.Service]
val horizontalComposeLayer: ZLayer[Any, Nothing, Has[Database.Service] with Has[Logger.Service]] = Database.live ++ Logger.live
Vertical Composition
When we have a layer that requires A
and produces B
, we can compose this layer with another layer that requires B
and produces C
; this composition produces a layer that requires A
and produces C
. The feed operator, >>>
, stack them on top of each other by using vertical composition.
This sort of composition is like function composition, feeding an output of one layer to an input of another.
In our case, our Users Service depends on both Logger Service and Database Service. As above we horizontally compose the Database and Logger layer so now we can use this horizontalcomposeLayer for vertical composition.
val combinedLayer: ZLayer[Any, Nothing, Has[Users.Service]] = horizontalComposeLayer >>> Users.live
We use the output of horizontalcomposeLayer
as input of Users.live
.We, therefore, obtain a single ZLayer
which contains the implementation of a Users.Service
, and the creation/passing of the Database.Service
and Logger.Service
happens because of the construction of horizontalcomposeLayer
(which contains implementations for both) and the >>>
operator, which then calls the callback from ZLayer.fromService
s.
Run the app
We can run the app by overriding the run method of zio.App trait which provides all the required environment.
object Main extends zio.App {
val program: ZIO[Users, Throwable, Unit] = for {
id <- ZIO(UUID.randomUUID().toString)
_ <- UserRepo.getUser(id)
} yield ()
val horizontalComposeLayer: ZLayer[Any, Nothing, Has[Database.Service] with Has[Logger.Service]] = Database.live ++ Logger.live
val combinedLayer: ZLayer[Any, Nothing, Has[Users.Service]] = horizontalComposeLayer >>> Users.live
override def run(args: List[String]): URIO[ZEnv, ExitCode] = program.provideSomeLayer(combinedLayer).exitCode
}
Complete code on this link.
Conclusion
We learn about how we compose layers in ZIO to create complex applications using the different features of ZIO. As there are different needs required for the composition of layers every time so it is very beneficial to first compose all the horizontally composed layers first.