Introducing Scopes in ZIO 2.0

Say Goodbye to ZManaged and Hello to Scope!

Pondering the implications

This is definitely a dramatic 11th hour change to ZIO, and I know there are a lot of people like me who have been eagerly awaiting 2.0 for a year. And I can only imagine all the people working on ZIO ecosystem projects who have been rushing to get their 2.0-compatible releases ready. I’m sure there was some pain removing all the Has references from code and documentation for RC1, but factoring out all the ZManaged!?

(Re)Working a Simple Example

I’ve created a simple sample project here in github in case you want to play around with this yourself. The project has two modules: “old” with ZIO 2.0.0-RC2 as its dependency and “new” which uses ZIO 2.0.0-RC4.

val fileReader =
ZManaged.acquireReleaseAttemptWith(Source.fromFile("build.sbt"))
(_.close())
val getLineCount: ZIO[Any, Throwable, Int] = fileReader.use { f =>
Task.attemptBlocking(f.getLines().size())
}
val fileReader = ZManaged.fromAutoCloseable {
ZIO.attemptBlocking(Source.fromFile("build.sbt"))
}

Managed Resources with Layers

The easier way to work with Managed resources that aren’t just going to be immediate one-off acquire/release implementations is to use Layers. So let’s create an alternative fileLayer object like this:

val fileLayer: ZLayer[Any, Throwable, Iterator[String] =
fileReader.map(_.getLines()).toLayer
val countLinesProgram: ZIO[Iterator[String], Throwable, Int] = for {
data <- ZIO.service[Iterator[String]]
size <- ZIO.attempt(data.size)
} yield size
override def run: ZIO[ZEnv, Any, Any] = {
for {
data <- fileReader.use(f =>
Task.attemptBlocking(f.getLines().size))
data2 <- countLinesProgram.provideLayer(fileLayer)
_ <- Console.printLine(s"$data, $data2")
} yield data
}

Shifting to “Scope”

Starting with 2.0.0-RC3, the entire ZManaged class is gone, so I don’t have the ZManaged.acquireReleaseWith or ZManaged.fromAutoCloseable methods, nor can I call .toLayer on a ZManaged anymore. What do I do?

val fileReader = 
ZIO.fromAutoCloseable(
ZIO.attemptBlockingIO(Source.fromFile("build.sbt")))
val read = fileReader.flatMap(f => 
ZIO.attemptBlockingIO(f.getLines().size))
val read = ZIO.scoped { 
fileReader.flatMap(f => ZIO.attemptBlockingIO(f.getLines().size))
}

Scoped Layers

(Warning: I’m going to start with a counterexample. I actually did this while writing the article, and Adam Frasier was kind enough to point out what I did.) Recreating the fileLayer from the ZManaged example is pretty straightforward: I can just call .toLayer on my fileReader object:

val fileLayer: ZLayer[Scope, IOException, Iterator[String]] = 
fileReader.map(_.getLines()).toLayer
val fileLayer: ZLayer[Scope, IOException, Iterator[String]] =
ZLayer.fromZIO(fileReader.map(_.getLines()))
val countLinesProgram: ZIO[Iterator[String], IOException, Int] = for {
data <- ZIO.service[Iterator[String]]
size <- ZIO.attemptBlockingIO(data.size)
} yield size
val toRun: ZIO[Scope, IOException, Int] =
countLinesProgram.provideLayer(fileLayer)

Important: the better way to construct the ZLayer!

When I defined my fileLayer object by calling .toLayer on my scoped fileReader object, I was just throwing a scoped ZIO object into a layer, thus the inherited Scope requirement.

val fileLayer: ZLayer[Any, IOException, Iterator[String]] = 
ZLayer.scoped(fileReader.map(_.getLines())

Running the program

Recall that the final ZManaged run program was written as follows:

override def run: ZIO[Any, Throwable, Int] = {
for {
data <- fileReader.use(f => Task.attempt(f.getLines().size))
data2 <- countLinesProgram.provideLayer(fileLayer)
_ <- Console.printLine(s"$data, $data2")
} yield data
}
override def run: ZIO[Any, IOException, Int] = {
for {
data <- ZIO.scoped(fileReader.flatMap(f =>
Task.attemptBlockingIO(f.getLines().size)) )
data2 <- ZIO.scoped(countLinesProgram.provideLayer(fileLayer))
_ <- Console.printLine(s"$data, $data2")
} yield data
}
override def run: ZIO[Any, IOException, Int] = {
ZIO.scoped {
for {
data <- fileReader.flatMap(f =>
Task.attemptBlockingIO(f.getLines().size))
data2 <- countLinesProgram.provideLayer(fileLayer)
_ <- Console.printLine(s"$data, $data2")
} yield data
}
}
override def run: ZIO[Scope, IOException, Int] = {
for {
data <- fileReader.flatMap(f =>
Task.attemptBlockingIO(f.getLines().size))
data2 <- countLinesProgram.provideLayer(fileLayer)
_ <- Console.printLine(s"$data, $data2")
} yield data
}

In conclusion…

I’ll let you judge for yourself. These eleventh-hour changes require me to re-wrap my brain around the ZIO idiosyncrasies, and I’m sure it’ll cause people some complications as they figure out how to migrate, but reflecting on the following notes from the ZIO News announcement:

--

--

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
Murray Todd Williams

Murray Todd Williams

Life-long learner, foodie and wine enthusiast, living in Austin, Texas.