How to Compose Layers in Zio

Reading Time: 5 minutes
Horizontal and Vertical Composition

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

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.ZLayers 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.fromServices.

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.

knoldus

Written by 

Akash Kumar is a Software Consultant at Knoldus Software LLP. He has done B.tech from Abdul Kalam Technical University. He is majorly focused on Scala and Angular. On the personnel side, he loves to play Cricket and online video games.

Leave a Reply