ZManaged
A ZManaged[R, E, A]
is a managed resource, that requires an R
, and may fail with an E
value, or succeed with an A
.
ZManaged
is a data structure that encapsulates the acquisition and the release of a resource, which may be used by invoking the use
method of the resource. The resource will be automatically acquired before the resource is used and automatically released after the resource is used.
Resources do not survive the scope of use
, meaning that if we attempt to capture the resource, leak it from use
, and then use it after the resource has been consumed, the resource will not be valid anymore and may fail with some checked error, as per the type of the functions provided by the resource.
Creationβ
In this section, we explore some common ways to create managed resources.
Makingβ
ZManaged
has a make
constructor which requires acquire
and release
actions:
val managed = ZManaged.make(acquire)(release)
In the following example, we have a managed resource which requires Console
as an environment to print the first line of a given file. The BufferedReader
will be acquired before printing the first line and automatically will be released after using BufferedReader
:
import zio.console._
def printFirstLine(file: String): ZIO[Console, Throwable, Unit] = {
def acquire(file: String) = ZIO.effect(new BufferedReader(new FileReader(file)))
def release(reader: BufferedReader) = ZIO.effectTotal(reader.close())
ZManaged.make(acquire(file))(release).use { reader =>
putStrLn(reader.readLine())
}
}
If we need to have different logic in release
action based on exit status of acquire
action, we can use ZManaged.makeExit
constructor:
trait ZManaged[-R, +E, +A] {
def makeExit[R, R1 <: R, E, A](
acquire: ZIO[R, E, A]
)(release: (A, Exit[Any, Any]) => ZIO[R1, Nothing, Any]): ZManaged[R1, E, A]
}
Not that like ZManaged.make
, both of acquire
and release
actions are uninterruptible in ZManaged.makeExit
.
Lifting a pure valueβ
We can lift pure values to ZManaged
with ZIO.succeed
method:
val managedString = ZManaged.succeed("Hello, World!")
val managedBoolean = ZManaged.succeed(true)
Lifting a ZIO effectβ
Every ZIO
effect can be lifted to ZManaged
with ZManaged.fromEffect
or ZIO#toZManaged_
operations:
val managedHello = ZManaged.fromEffect(putStrLn("Hello, World!"))
val managedHello_ = putStrLn("Hello, World!").toManaged_
This is useful when we want to combine ZManaged
effects with ZIO
effects. Assume during creation of managed resource, we need to log some information, we can lift a ZIO
effect to ZManaged
world:
def userRepository: ZManaged[Blocking with Console, Throwable, UserRepository] = for {
cfg <- dbConfig.toManaged_
_ <- putStrLn("Read database config").toManaged_
_ <- initializeDb(cfg).toManaged_
_ <- putStrLn("Database initialized").toManaged_
xa <- makeTransactor(cfg)
_ <- putStrLn("Created new blocking transactor").toManaged_
} yield new UserRepository(xa)
Making from AutoClosable Resourcesβ
If the resource implemented the AutoClosable
interface, we can easily make a ZManaged
from it by using ZManaged.fromAutoClosable
constructor:
ZManaged.fromAutoCloseable(ZIO.effect(new FileInputStream("file.txt")))
// res1: ZManaged[Any, Throwable, FileInputStream] = zio.ZManaged$$anon$2@19dd8c22
ZManaged.fromAutoCloseable(ZIO.effect(fromResource("file.txt")))
// res2: ZManaged[Any, Throwable, scala.io.BufferedSource] = zio.ZManaged$$anon$2@60c09e2e
ZManaged.fromAutoCloseable(ZIO.effect(fromFile("file.txt")))
// res3: ZManaged[Any, Throwable, scala.io.BufferedSource] = zio.ZManaged$$anon$2@210bd8dd
Making Interruptible Acquiresβ
By default, when we create a ZManaged
via ZManaged.make
constructor, the acquire
and release
actions are uninterruptible. But what if we want to make the acquire
action interruptible? The makeInterruptible
constructor does that for us:
trait ZManaged[-R, +E, +A] {
def makeInterruptible[R, E, A](
acquire: ZIO[R, E, A]
)(release: A => URIO[R, Any]): ZManaged[R, E, A]
}
Making ZManaged
via this constructor makes the acquire
action interruptible, the release action remains uninterruptible.
If we want to decide what to do in the release
action based on how the acquire
action is completed, whether by success, failure, or interruption; we can use the makeReserve
constructor. The type of release
action is Exit[Any, Any] => URIO[R, Any]
which provides us the Exit
status of the acquire
action, so we can decide what to do based on the exit status of acquire
action:
trait ZManaged[-R, +E, +A] {
def makeReserve[R, E, A](reservation: ZIO[R, E, Reservation[R, E, A]]): ZManaged[R, E, A]
}
Reservation
data type is defined as follows:
final case class Reservation[-R, +E, +A](acquire: ZIO[R, E, A], release: Exit[Any, Any] => URIO[R, Any])
Usageβ
useβ
Inside the use
block, we can use the managed resource and return a new value. The use
method converts a managed resource from ZManaged
world to ZIO
world:
def firstLine(file: String): ZIO[Console, Throwable, Unit] =
ZManaged.fromAutoCloseable(ZIO.effect(fromFile(file))).use { reader =>
putStrLn(reader.bufferedReader().readLine())
}
useNowβ
If our managed resource could be valid after releasing resources, we can convert that ZManaged
to ZIO
effect by calling ZManaged#useNow
.
val hello: UIO[String] = ZManaged.succeed("Hello, World!").useNow
This is useful when we have composed some ZManaged
with some ZIO
effects, and the result can be outlived outside the use
block:
def is(file: String): Task[FileInputStream] = Task.effect(???)
def os(file: String): Task[FileOutputStream] = Task.effect(???)
def close(resource: Closeable): UIO[Unit] = Task.effectTotal(???)
def copy(from: FileInputStream, to: FileOutputStream): Task[Unit] = ???
def transfer(from: String, to: String): ZIO[Any, Throwable, Unit] = {
val resource: ZManaged[Any, Throwable, Unit] = for {
from <- ZManaged.make(is(from))(close)
to <- ZManaged.make(os(to))(close)
_ <- copy(from, to).toManaged_
} yield ()
resource.useNow
}
Note:
Be careful, donβt call the
useNow
method on a managed resource that isnβt valid after its release actions. For example, runninguseNow
onZManaged.fromAutoCloseable(ZIO.effect(fromFile("file.txt")))
doesnβt make sense, because after releasing a file, we havenβt any handle to that file.
useForeverβ
Assume we are going to make a managed resource long-lived. The ZManaged#useForever
does that for us, it converts a ZManaged
effect to a ZIO
effect which will remain forever running.
Combinatorsβ
ZManaged
like the ZIO
effect has almost all combinators that we introduced on the ZIO page. We can use them to create more complicated ZManaged
ones.
There is also some combinators which specific for ZManaged
:
- ensuringFirstβ This combinator adds a
ZIO
effect as a finalizer to an existingZManaged
effect. This finalizer will be executed before the existing finalizers:
trait ZManaged[-R, +E, +A] {
def ensuringFirst[R1 <: R](f: ZIO[R1, Nothing, Any]): ZManaged[R1, E, A]
}
- onExitFirstβ Like
ensuringFirst
, but it has access to theZManaged
βs result:
trait ZManaged[-R, +E, +A] {
def onExitFirst[R1 <: R](cleanup: Exit[E, A] => ZIO[R1, Nothing, Any]): ZManaged[R1, E, A]
}
- withEarlyReleaseβ It will produce another
ZManaged
which provides a canceler that can be used to eagerly execute the finalizer of thisZManaged
:
trait ZManaged[-R, +E, +A] {
def withEarlyRelease: ZManaged[R, E, (UIO[Any], A)]
}
- withEarlyReleaseExitβ Like
withEarlyRelease
, but allows us to specify an exit value in the event of early release:
trait ZManaged[-R, +E, +A] {
def withEarlyReleaseExit(e: Exit[Any, Any]): ZManaged[R, E, (UIO[Any], A)]
}