As you may already know, ZIO is designed around three type parameters:
ZIO[-R, +E, +A]
You may also remember that a nice mental model of the ZIO data type is the following:
ZEnvironment[R] => Either[E, A]
This means a ZIO effect needs an environment of type ZEnvironment[R] to run (we will discuss in the following section in more detail about this ZEnvironment type), hence we need to fulfill this requirement in order to make the effect runnable. More concretely, this ZEnvironment[R]type represents a dependency on a service or several services that are needed for running the effect. Therefore, let’s now discuss how Services are defined in ZIO (by the way, if you need a more in-depth introduction to ZIO, you can take a look at this article in the Scalac blog).
As mentioned in the ZIO documentation page: “A service is a group of functions that deals with only one concern. Keeping the scope of each service limited to a single responsibility improves our ability to understand code, in that we need to focus only on one topic at a time without juggling too many concepts together in our head”.
The idea is that ZIO allows us to define services and use them to create different application layers that rely on each other. This means each layer depends on the layers immediately below it, although it doesn’t know anything about their implementation details. This is a really powerful concept because it improves composability and testability (because you can easily change each of the service’s implementations without affecting other layers).
Now, if you are thinking about how to define these services, ZIO provides us with a nice recipe to follow when defining a new service. This recipe should be familiar to object-oriented programmers:
Don’t worry if this all seems too abstract at the moment because we are going to be applying this recipe to implement the Tic-Tac-Toe application later. The only important thing, for now, is to get to know ZLayer, a very important data type mentioned in this recipe, and which is related to another very important one: ZEnvironment. So let’s discuss those now.
As mentioned in the ZIO documentation page, 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 (you can think of it as a Map[ServiceType, ServiceImplementation]) to maintain all the environmental services and their implementations.
It’s important to mention that ZEnvironment replaces the old Has data-type of ZIO 1.0, which wasn’t very user-friendly. Also, ZEnvironment is now subsumed into the ZIO data type itself, which improves its usability even further.
Let’s now see a very simple example of how ZEnvironment can be used. Let’s say we have a getCurrentUser effect that requires some services (Logging and HttpClient) from the environment:
val getCurrentUser: URIO[Logging with HttpClient, User] = ???
Our services are defined like this:
trait Logging // Service interface
final case class LoggingLive() extends Logging // Service implementation
trait HttpClient
final case class HttpClientLive() extends HttpClient
So, in order for ZIO to be able to execute the getCurrentUser effect, we need to provide the dependencies it needs. For that, we first need to create a ZEnvironment containing all the required dependencies:
val env: ZEnvironment[Logging with HttpClient] =
ZEnvironment(LoggingLive(), HttpClientLive())
Now, we can provide this environment to getCurrentUser, by calling ZIO#provideEnvironment:
val getCurrentUserWithEnv: UIO[User] = getCurrentUser.provideEnvironment(env)
And now, we have a ZIO effect that does not require any environment, because we have provided all the required dependencies, and ZIO will be able to execute it. Now a final word about ZEnvironment, and it’s that normally you won’t need to work directly with it. Because there’s a more powerful data type that you can use instead to provide required dependencies to a ZIO effect: ZLayer.
The ZLayer data type is an immutable value which contains a pure description for building a ZEnvironment[ROut], starting from a value RIn, possibly producing an error E during creation:
ZLayer[-RIn, +E, +ROut]
If you think about it, ZLayer is a bit like a class constructor. However, while a class constructor describes how you build objects of some class, it doesn’t describe the process as a value, but ZLayer does! So:
Because ZLayers are values, they are highly compositional, and can be combined in two fundamental ways:
Again, don’t panic if this doesn’t make too much sense for you at the moment, because we are going to be applying both the horizontal and vertical compositions when we implement the Tic-Tac-Toe application and everything will become clearer.
Now you may be thinking: Do we really need ZLayer? Isn’t ZEnvironment enough to provide dependencies to a ZIO effect?
The answer is that, in small applications with a limited number of services, ZEnvironment would be enough. However, in real life a lot of applications consist of thousands or millions of lines of code, contain several different services and have different test and production implementations for each service. Manually wiring all of these services becomes a tedious exercise, and it’s an opportunity for people to make the same mistakes over and over again.
What you want instead is some automatic Dependency Injection mechanism, which gives you structure, lots of it, so you basically make it very simple for people to add new services, new implementations, and enforce best practices being followed. That’s what ZLayer is all about! It helps you to structure large-scale applications in a way that scales.
By the way, best practices that are automatically enforced by ZLayer (you don’t even need to think about them!) include the following:
Finally, it’s worth mentioning that ZIO provides some type aliases for the ZLayer data type which are very useful when representing some common use cases. The good news is that the logic for defining these type aliases is practically the same as that applied for defining the ZIO type aliases. Here’s the complete list:
Now, if you are wondering how to create and use ZLayers, stay tuned because we are going to be seeing how easy that is to do in the next section.