Migrating ZIO environment from RC-17 to RC-18

Wiem Zine
5 min readMar 18, 2020

--

In this blog post I will show you how to migrate from RC-17 to RC-18 taking the ZIO with http4s and doobie example and using the recent ZIO version: “1.0.0-RC18–2”

Inspired by:

The major changes was basically in ZIO Environment, let’s take the example of Configuration and Persistence dependencies:

Configuration

1- Module

Using RC-17:

Configuration module has a reference to the interface Configuration.Service

trait Configuration {
val config: Configuration.Service
}

Using RC-18:

Configuration module has the interface Configuration.Service

type Configuration = Has[Config.Service]

Has is a data type that has been introduced in ZIO RC-18.

2- Interface

The interface Configuration.Service contains the definition of the methods provided by the module Configuration

The same: Using RC-17 and RC-18

object Configuration {
trait Service {
def load: Task[Config]
}
}

3- Helpers

The helpers access the Configuration module and delegate to the functions inside the interface Configuration.Service.

We can access to the module using ZIO#access* and we define the helpers in the package object which would be the scope in which you can find the provided methods of the specified dependency.

in our case we can define the helpers in the package configuration

Hint

Sometimes we define Service[R] : The parameter type R is useful to guarantee that you have defined all the helper methods in the Service. So the package object have to extend Service[Module] :

then you implement the methods of Configuration.Service

package object configuration extends Configuration.Service[Configuration] {
def loadConfig: RIO[Configuration, Config] = ???
}

override def load: RIO[Configuration, Config] = ???

If you didn’t specify the type parameter R you can do that manually and you can name your helper methods as you want, for example:

package object configuration {
def loadConfig: RIO[Configuration, Config] = ???
}

Using RC-17:

We use the reference Configuration#config to access the methods defined in the interface Configuration.Service .

package object configuration {  def loadConfig: RIO[Configuration, Config] = 
RIO.accessM(_.config.load)
}

Using RC-18:

We call Has#get to access the methods defined in the interface Configuration.Service

package object configuration {  def loadConfig: RIO[Configuration, Config] = 
RIO.accessM(_.get.load)
}

4- Implementation (Production version of Configuration )

Using RC-17

trait Live extends Configuration {
val config: Service = new Service {
def load: Task[Config] = Task.effect(loadConfigOrThrow[Config])
}
}

object Live extends Live

Using RC-18

val live: Layer[Nothing, Configuration] = ZLayer.succeed(new Service {
def load: Task[Config] = Task.effect(loadConfigOrThrow[Config])
})

If the production implementation requires other dependencies, you can use: ZLayer[Dependency, Error, CurrentModule]

There are different ways to make a layer, you can create a layer from:

  • Service
  • An effect (like in the new Configuration implementation in the example) where you can load the configuration whenever you provide the layer and you can use DBConfig and ApiConfig separately:
val live: Layer[Throwable, Configuration] = ZLayer.fromEffectMany(
Task
.effect(loadConfigOrThrow[AppConfig])
.map(c => Has(c.api) ++ Has(c.dbConfig)))
  • Managed (to manage your resources, example: DBClient, KinesisClient, SqsClient…), example:
val live: Layer[Nothing, SqsClient] = ZLayer.fromManaged(m)
val m = Managed.make(createSqsClient)(_.shutdown)

5- Provide the implementation

In the Main program that extends zio.App, you have to provide all the environments used in your App that are unknown for ZIO,

If you’re using one or more ZIO environment ( ZEnv) in your App environment, for example:

type AppEnv = Clock with Configuration with Blocking

You can provide only the Configuration using provideSome

Using RC-17

program.provideSome[ZEnv] { _ =>
new Clock.Live with Blocking.Live with Configuration.Live
}

Using RC-18

program.provideSomeLayer[ZEnv](Configuration.live)

Which makes it simpler

and if you have more layers to provide there are combinators that you can apply to the layers:

instead of (RC-17)

new Clock.Live with Blocking.Live with Configuration.Live

You can do: (RC-18)

Clock.live ++ Blocking.live ++ Configuration.live

Provide a specific environment:

Using RC-17

program.provide(Configuration.Live)

Using RC-18

program.provideLayer(Configuration.live)

Note: You can still use provide as before, but if you have provideSome where you need to provide an implementation for ZIO environment, you have to use ZLayers then you will be able to specify only your App environment using provideSomeLayer .

Persistence

The same thing for Persistence dependency:

1- Module

Using RC-17

trait Persistence {
val userPersistence: Persistence.Service[Any]
}

Using RC-18

type Persistence = Has[Persistence.Service[Any]]

2- Interface

The same: Using RC-17 and RC-18

object Persistence {
trait Service[R] {
def get(id: Int): RIO[R, User]
def create(user: User): RIO[R, User]
def delete(id: Int): RIO[R, Boolean]
}
}

3- Helpers

Using RC-17: Use PersistenceService#userPersistence to access the methods in the Service.

package object db extends Persistence.Service[Persistence] {

def get(id: Int): RIO[Persistence, User] =
RIO.accessM(_.userPersistence.get(id))
def create(user: User): RIO[Persistence, User] =
RIO.accessM(_.userPersistence.create(user))
def delete(id: Int): RIO[Persistence, Boolean] =
RIO.accessM(_.userPersistence.delete(id))
}

Using RC-18: Use Has#get to access the methods in the Service.

package object db extends Persistence.Service[Persistence] {

def get(id: Int): RIO[Persistence, User] =
RIO.accessM(_.get.get(id))
def create(user: User): RIO[Persistence, User] =
RIO.accessM(_.get.create(user))
def delete(id: Int): RIO[Persistence, Boolean] =
RIO.accessM(_.get.delete(id))
}

4- Implementation (Production version of Persistence )

RC-17:

trait Live extends Persistence {

protected def tnx: Transactor[Task]

val userPersistence: Service[Any] = new Service[Any] {

def get(id: Int): Task[User] =
SQL
.get(id)
.option
.transact(tnx)
.foldM(
err => Task.fail(err),
maybeUser =>
Task.require(UserNotFound(id))(Task.succeed(maybeUser))
)

def create(user: User): Task[User] =
SQL
.create(user)
.run
.transact(tnx)
.foldM(err => Task.fail(err), _ => Task.succeed(user))

def delete(id: Int): Task[Boolean] =
SQL
.delete(id)
.run
.transact(tnx)
.fold(_ => false, _ => true)
}
}
// make the transactor and use it transactorR.use { transactor =>
server.provideSome[ZEnv] { _ =>
new Clock.Live with Blocking.Live
with Persistence.Live {
override def tnx: doobie.Transactor[Task] = transactor
}
}

RC-18:

trait Live extends Service[Any] {

def get(id: Int): Task[User] =
SQL
.get(id)
.option
.transact(tnx)
.foldM(
err => Task.fail(err),
maybeUser =>
Task.require(UserNotFound(id))(Task.succeed(maybeUser))
)

def create(user: User): Task[User] =
SQL
.create(user)
.run
.transact(tnx)
.foldM(err => Task.fail(err), _ => Task.succeed(user))

def delete(id: Int): Task[Boolean] =
SQL
.delete(id)
.run
.transact(tnx)
.fold(_ => false, _ => true)
}
def live(transactor: Transactor[Task]): Layer[Nothing, Persistence] = ZLayer.succeed(Persistence.Live(transactor))// make the transactor and use ittransactorR.use ( transactor =>
server.provideSomeLayer[ZEnv](Persistence.live(transactor)) )

Summary

You can see the code of RC-17 (link) and the migration to RC-18–2 (link)

And the final version of the code here has more refactoring.

If you are interested to see the other changes made in ZIO-RC-18 checkout the release note and the documentation of modules and layers .

Zio! 👋

--

--