Skip to main content
Version: 1.0.18

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.

Overview

ZIO Wrap services with Has data type to:

  1. Combine multiple services together.
  2. Bind services into their implementations.

Combining Services

Two or more Has[_] elements can be combined horizontally using their ++ operator:

val logger: Has[Logging]   = Has(new Logging{})
val random: Has[RandomInt] = Has(new RandomInt{})

// Note the use of the infix `++` operator on `Has` to combine two `Has` elements:
val combined: Has[Logging] with Has[RandomInt] = logger ++ random

Binding Services

The extra power that is given by Has is that the resulting data structure is backed by an heterogeneous map. Has can be thought of as a Map[K, V] which keys are service types and values are service implementations. from service type to service implementation, that collects each instance that is mixed in so that the instances can be accessed/extracted/modified individually, all while still guaranteeing supreme type safety.

ZIO internally can ask combined using get method to determine binding configurations:

// get back the Logging and RandomInt services from the combined values:
val logger: Logging = combined.get[Logging]
val random: RandomInt = combined.get[RandomInt]

These are implementation details. Usually, we don't create a Has directly. Instead, we create a Has using ZLayer.

Motivation

Some components in an application might depend upon more than one service, so we might need to combine multiple services and feed them to the ZIO Environment. Services cannot directly be combined, they can be combined if they first wrapped in the Has data type.

Let's get into this problem and how the Has data type, solves this problem:

Problem

ZIO environment has a ZIO#provide which takes an R and returns a ZIO effect which doesn't require R and ready to be run by the unsafeRun operation of Runtime.

Assume we have two Logging and RandomInt services:

trait Logging {
def log(line: String): UIO[Unit]
}

trait RandomInt {
def random: UIO[Int]
}

We also provided their accessors to their companion object. We just used ZIO.accessM to access environment of each service:

object Logging {
def log(line: String): ZIO[Logging, Nothing, Unit] = ZIO.accessM[Logging](_.log(line))
}

object RandomInt {
val random: ZIO[RandomInt, Nothing, Int] = ZIO.accessM[RandomInt](_.random)
}

Now, we are ready to write our application using these interfaces. We are going to write a simple program which generates a new random number and feed it into the logger:

val myApp: ZIO[Logging with RandomInt, Nothing, Unit] = 
for {
_ <- Logging.log("Application Started!")
nextInt <- RandomInt.random
- <- Logging.log(s"Random number generated: ${nextInt.toString}")
} yield ()

To run this program, we need to implement a live version of Logging and RandomInt services. So let's implement each of them:

val LoggingLive: Logging = new Logging {
override def log(line: String): UIO[Unit] =
ZIO.effectTotal(println(line))
}

val RandomIntLive: RandomInt = new RandomInt {
override def random: UIO[Int] =
ZIO.effectTotal(scala.util.Random.nextInt())
}

Great! Now, we are ready to inject these two dependencies into our application myApp through ZIO.provide function.

lazy val mainApp = myApp.provide(???) //What to provide?

As the type of myApp effect is ZIO[Logging with RandomInt, Nothing, Unit], we should provide an object with a type of Logging with RandomInt. Oh! How can we combine LoggingLive and RandomIntLive objects together? Unfortunately, we don't have a way to combine these two objects to create a required service (Logging with RandomInt).

But, there is a workaround, we can throw away these implementations and write a new implementation for an intersection of these two services:

val LoggingWithRandomIntLive = new Logging with RandomInt {
override def log(line: String): UIO[Unit] =
ZIO.effectTotal(println(line))

override def random: UIO[Int] =
ZIO.effectTotal(scala.util.Random.nextInt())
}

Now, we can provide this implementation into our application:

val mainApp: IO[Nothing, Unit] = myApp.provide(LoggingWithRandomIntLive)

The mainApp doesn't need any environmental services and can be run by using the ZIO Runtime system:

Runtime.default.unsafeRun(mainApp)

But this workaround is not perfect, because every time we are writing an application, we need to provide a specific implementation for its requirement. This is overwhelming.

We need to implement each of each service separately and at the end of the day, combine them and provide that to our application. This is where the Has[_] wrapper data type comes into play.

Solution

Has[_] data type enables us to combine different services and provide them to the ZIO Environment. Let's solve the previous problem by using the Has wrapper.

First, we should change the accessor methods to return us an effect which requires services wrapped into the Has data type:

object Logging {
def log(line: String): ZIO[Has[Logging], Nothing, Unit] =
ZIO.serviceWith[Logging](_.log(line))
}

object RandomInt {
val random: ZIO[Has[RandomInt], Nothing, Int] =
ZIO.serviceWith[RandomInt](_.random)
}

ZIO.serviceWith is accessor method like ZIO.accessM, it accesses the specified service in the environment of effect, but it returns a ZIO effect which requires a service wrapped in Has[_] data type.

We should refactor our application to represent the correct types.

val myApp: ZIO[Has[Logging] with Has[RandomInt], Nothing, Unit] =
for {
_ <- Logging.log("Application Started!")
nextInt <- RandomInt.random
- <- Logging.log(s"Random number generated: ${nextInt.toString}")
} yield ()

Now, our application is a ZIO effect which requires Has[Logging] with Has[RandomInt] services. Let's combine implementation of these two services using Has data type:

val combined: Has[Logging] with Has[RandomInt] = Has(LoggingLive) ++ Has(RandomIntLive)

Let's feed the combined services into our application:

val effect: IO[Nothing, Unit] = myApp.provide(combined)
zio.Runtime.default.unsafeRun(effect)

That is how the Has data type helps us to combine services. The previous example was just for demonstrating purposes, and we rarely create Has data type directly. Instead, we create a Has via ZLayer.

Whenever we lift a service value into ZLayer with the ZLayer.succeed constructor or toLayer, ZIO will wrap our service with Has data type.

Let's implement Logging and RandomInt services:

case class LoggingLive() extends Logging {
override def log(line: String): UIO[Unit] =
ZIO.effectTotal(println(line))
}

case class RandomIntLive() extends RandomInt {
override def random: UIO[Int] =
ZIO.effectTotal(scala.util.Random.nextInt())
}

Now, we can lift these two implementations into the ZLayer. The ZLayer will wrap our services into the Has[_] data type:

object LoggingLive {
val layer: URLayer[Any, Has[Logging]] =
(LoggingLive.apply _).toLayer
}

object RandomIntLive {
val layer: URLayer[Any, Has[RandomInt]] =
(RandomIntLive.apply _).toLayer
}

Now, when we combine multiple layers together, these services will combined via with intersection type:

val myLayer: ZLayer[Any, Nothing, Has[Logging] with Has[RandomInt]] = 
LoggingLive.layer ++ RandomIntLive.layer

Finally, when we provide our layer into the ZIO effect, ZIO can access the binding configuration and extract each service. ZIO does internally these pieces of wiring machinery, we don't care about the implementation detail:

val mainApp: ZIO[Any, Nothing, Unit] = myApp.provideLayer(myLayer) 
// mainApp: ZIO[Any, Nothing, Unit] = zio.ZIO$CheckInterrupt@4682e5d9