What are the benefits of the ZIO modules with ZLayers

Three months ago I wrote this Blog:

Meanwhile ZIO took really of and one of my concerns got a major update with RC18:

I still struggle with ZIO, for example providing the runtime/ environment…

So I updated all my ZIO projects with ZLayers!

In this Blog I will focus on my experiences I made when migrating to the new Modules and Layers of ZIO.

Spoiler: The migration was straight forward!

I structured this Blog in three parts:

  1. Listing the Module parts as they were in my project.

1. Old Style

Image for post
Image for post
As I don’t have a cat — a Bike photo 😏

So how did a Module look like before RC18? I just extracted the main parts of the official documentation. And there were quite some parts:

Module definition

trait Components extends Serializable {
val components: Components.Service[ComponentsEnv]
}

The Components trait is the module, which is just a container for the Components.Service.

Service

object Components {  trait Service[R] {
def load[T <: Component : ClassTag](ref: CompRef): RIO[R, T]
def render(component: Component): RIO[R, String]
}
}

The service is just an ordinary interface, defining the capabilities it provides.

  • By convention it is in the modules companion object.

Access Methods

The Access Methods allow you to use the Service without knowing which Implementation it gonna be. As you can see they are in a strangely named object. For example:Components.>.render(myComp)

Implementation

Each implementation can specify its own requirements (environment R). In our example ComponentsEnv which resolves to the standard Console.

The Program

The program itself does not care what its implementation is. All it says it requires an AppEnv in the end.

Run the Program

And here at last we provide our Program with the implementations. As seen above it requires aConsole and a Components implementation.

2. New Style

Image for post
Image for post

Ok, now the actual part we are exited about.

So let’s see if ZLayers are so awesome.

I will not explain the different concepts and data types, as they are well explained in the official documentation.

Module definition

type Components = Has[Components.Service]

The Components is just a type alias for the ‘module’, which describes a dependency to the service Components.Service.

Has[A] represents a dependency on a service of type A.

Service

The only difference here is that the environment R is not set on the Service but on the methods. So each functionality defines its on requirements.

Also interesting is that all Service implementations have the same requirements. This means if you need different environments in your implementation, you cannot provide it via the environment R anymore.

Access Methods

Nothing much changed here:

  • There is no extra object for the Access Methods.

Implementation

def live(service: Service): ZLayer.NoDeps[Nothing, Components] = ZLayer.succeed(service)

The implementation is now just creating a ZLayer with a Service.

In our case as we have different Service implementations we provide a function that takes a Service implementation and creates a ZLayer from it.

The Service implementation looks the same as before, except that we do not need to implement the whole Module anymore!

The Program

No change here, I had only to adjust the imports. From Components.>.{load, render}toComponents.{load, render}.

Run the Program

Two things have changed:

  • We must not provide the standard environment Console.

How to pass implementation specific environments?

There was one thing that I did not grasp at first. If the Service implementation can not have its own environment R. How would you achieve this now?

My first thought was to just add it to the Service definition.

trait Service { 
def myFunct(): RIO[Console, String]
}

This implies that every implementation needs to be adjusted, if you want to add an environment (e.g. Clock). So no that would be really bad.

Ok why not just add it as a dependency, like:

def live(console: Console): ZLayer.NoDeps[Nothing, NumberService] = ZLayer.succeed(new Service{
def myFunct(): IO[String] = console.putStrLine....
})

Better, but now the question arises — why would we need the environment at all?

The answer of course was Composing ZLayers vertically. So our implementation looks now:

val live: ZLayer[Console, Nothing, NumberService] =  
ZLayer.fromFunction{ console: Console =>
console.putStrLine....

And here is how you can provide it:

...}.provideCustomLayer((Console.live >>> NumberService.live)

Improvements

Image for post
Image for post

Here are my personal Top five improvements of this redesign:

  1. The Environment is now set on each Functionality. So not all functionalities must have the same Requirements. Also the whole setup is now much cleaner without R everywhere — or was it Any😏.

Conclusion

This update makes modules more accessible and likely to be adopted. One step closer to production ready.

And yes ZLayers are awesome — can’t wait for ZIO Release 1.0!

References

The Github Projects that I migrated and used in this Blog:

I migrated the Github project according to this documentation:

The old version of the module doc can be found here:

The images are from Central Switzerland — so if you are in the region && (want to Bike || talk about ZIO) > let me know😏.

Written by

Working for finnova.com in the Banking business. Prefer to work with Scala / ScalaJS.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store