ZEnvironment
A ZEnvironment[R]
is a built-in type-level map for the ZIO
data type which is responsible for maintaining the environment of a ZIO
effect. The ZIO
data type uses this map to maintain all the environmental services and their implementations.
For example, assume we have written a ZEnvironment
containing all built-in services as below:
import zio._
val environment: ZEnvironment[Console & Clock & Random & System] =
ZEnvironment[Console, Clock, Random, System](
Console.ConsoleLive,
Clock.ClockLive,
Random.RandomLive,
System.SystemLive
)
This map contains all built-in services and their corresponding implementations. If we evaluate the ZEnvironment#toString
method, we can see the underlying type-level map something like this.
ZEnvironment(
Map(
Console -> (zio.Console$ConsoleLive$@76a3e297, 0),
Clock -> (zio.Clock$ClockLive$@4d3167f4, 1),
Random -> (RandomScala(scala.util.Random$@4eb7f003), 2),
System -> (zio.System$SystemLive$@eafc191, 3)
)
)
From a ZIO environment point of view, we can think of ZIO
as the following function:
type ZIO[R, E, A] = ZEnvironment[R] => Either[E, A]
or
type ZIO[R, E, A] = ZEnvironment[R] => IO[E, A]
For example, the ZIO[Foo & Bar, Throwable, String]
can be thought of as a function from ZEnvironment[Foo & Bar]
to Either[Throwable, String]
:
The ZEnvironment
is useful for manually constructing and combining the ZIO environment. So, in most cases, we do not require working directly with this data type. So you can skip reading this page if you are not an advanced user.
We can eliminate the environment of ZIO[R, E, A]
by providing ZEnvironment[R]
to that effect.
Also, we can access the whole environment using ZIO.environment
:
import zio._
import java.io.IOException
case class AppConfig(poolSize: Int)
val myApp: ZIO[AppConfig, IOException, Unit] =
ZIO.environment[AppConfig].flatMap { env =>
val config = env.get[AppConfig]
Console.printLine(s"Application started with config: $config")
}
val eliminated: IO[IOException, Unit] =
myApp.provideEnvironment(
ZEnvironment(AppConfig(poolSize = 10))
)
In most cases, we do not require using ZIO.environment
to access the whole environment or the ZIO#provideEnvironment
to provide effect dependencies. Therefore, most of the time, we use ZIO.service*
and other ZIO#provide*
methods to access a specific service from the environment or provide services to a ZIO effect.
Creation
To create an empty ZIO environment:
import zio._
val empty: ZEnvironment[Any] = ZEnvironment.empty
To create a ZIO environment from a simple value:
import zio._
case class AppConfig(host: String, port: Int)
val config: ZEnvironment[AppConfig] = ZEnvironment(AppConfig("localhost", 8080))
Operations
To combine two or multiple environment we can use union
or ++
operator:
import zio._
case class AppConfig(host: String, port: Int)
val app: ZEnvironment[AppConfig] =
ZEnvironment.empty ++ ZEnvironment(AppConfig("localhost", 8080))
To add a service to an environment:
import zio._
case class AppConfig(host: String, port: Int)
val app: ZEnvironment[AppConfig] =
ZEnvironment.empty.add(AppConfig("localhost", 8080))
To retrieve a service from the environment, we use get
method:
import zio._
case class AppConfig(host: String, port: Int)
val app: ZEnvironment[AppConfig] =
ZEnvironment.empty.add(AppConfig("localhost", 8080))
val appConfig: AppConfig = app.get[AppConfig]
Providing Multiple Instance of the Same Interface
We can express an effect's dependency on multiple services of the type A
which are keyed by type K
with Map[K, A]
. For example, the ZIO[Map[String, Database], Throwable, Unit]
is an effect that depends on multiple Database
versions.
To access the specified service corresponding to a specific key, we can use the ZIO.serviceAt[Service](key)
constructor. For example, to access a Database
service which is specified by the "inmemory" key, we can write:
val database: URIO[Map[String, Database], Option[Database]] =
ZIO.serviceAt[Database]("inmemory")
A service can be updated at the specified key using the ZIO#updateServiceAt
operator.
Multiple Config Example
Let's see how we can create a layer comprising multiple instances of AppConfig
:
import zio._
case class AppConfig(host: String, port: Int)
object AppConfig {
val layer: ULayer[Map[String, AppConfig]] =
ZLayer.succeedEnvironment(
ZEnvironment(
Map(
"prod" -> AppConfig("production.myapp", 80),
"dev" -> AppConfig("development.myapp", 8080)
)
)
)
}
And here is the application which uses different AppConfig
from the ZIO environment based on the value of the APP_ENV
environment variable:
import zio._
object MultipleConfigExample extends ZIOAppDefault {
val myApp: ZIO[Map[String, AppConfig], String, Unit] = for {
env <- System.env("APP_ENV")
.flatMap(x => ZIO.fromOption(x))
.orElseFail("The environment variable APP_ENV cannot be found.")
config <- ZIO.serviceAt[AppConfig](env)
.flatMap(x => ZIO.fromOption(x))
.orElseFail(s"The $env config cannot be found in the ZIO environment")
_ <- ZIO.logInfo(s"Application started with: $config")
} yield ()
def run =
myApp.provide(AppConfig.layer)
}
Multiple Database Example
Here is an example of providing multiple instances of the Database
service to the ZIO environment:
import zio._
import java.nio.charset.StandardCharsets
trait Database {
def add(key: String, value: Array[Byte]): ZIO[Any, Throwable, Unit]
}
object Database {
val layer: ULayer[Map[String, Database]] = {
ZLayer.succeedEnvironment(
ZEnvironment(
Map(
"persistent" -> PersistentDatabase.apply(),
"inmemory" -> InmemoryDatabase.apply()
)
)
)
}
}
case class InmemoryDatabase() extends Database {
override def add(key: String, value: Array[Byte]): ZIO[Any, Throwable, Unit] =
ZIO.unit <* ZIO.logInfo(s"new $key added to the inmemory database")
}
case class PersistentDatabase() extends Database {
override def add(key: String, value: Array[Byte]): ZIO[Any, Throwable, Unit] =
ZIO.unit <* ZIO.logInfo(s"new $key added to the persistent database")
}
object MultipleDatabaseExample extends ZIOAppDefault {
val myApp = for {
inmemory <- ZIO.serviceAt[Database]("inmemory")
.flatMap(x => ZIO.fromOption[Database](x))
.orElseFail("failed to find an in-memory database in the ZIO environment")
persistent <- ZIO.serviceAt[Database]("persistent")
.flatMap(x => ZIO.fromOption[Database](x))
.orElseFail("failed to find an persistent database in the ZIO environment")
_ <- inmemory.add("key1", "value1".getBytes(StandardCharsets.UTF_8))
_ <- persistent.add("key2", "value2".getBytes(StandardCharsets.UTF_8))
} yield ()
def run = myApp.provideLayer(Database.layer)
}