问题
I have a heavy side-effecting function (think database call) that I want to use as a lazy value, so that it gets called only on first use (and not at all if never used).
How do I do this with ZIO?
If my program looks like this, the function gets called only once (but even the result is not used at all):
import scalaz.zio.IO
import scalaz.zio.console._
object Main extends scalaz.zio.App {
def longRunningDbAction: IO[Nothing, Integer] = for {
_ <- putStrLn("Calling the database now")
} yield 42
def maybeUseTheValue(x: Integer): IO[Nothing, Unit] = for {
_ <- putStrLn(s"The database said ${x}")
} yield ()
def maybeNeedItAgain(x: Integer): IO[Nothing, Unit] = for {
_ <- putStrLn("Okay, we did not need it again here.")
} yield ()
override def run(args: List[String]): IO[Nothing, Main.ExitStatus] = for {
valueFromDb <- longRunningDbAction
_ <- maybeUseTheValue(valueFromDb)
_ <- maybeNeedItAgain(valueFromDb)
} yield ExitStatus.ExitNow(0)
}
I suppose I have to pass an IO
that produces the Int
instead of the already materialized Int
, but if I pass in the original IO
that just calls the database, it will be called repeatedly:
object Main extends scalaz.zio.App {
def longRunningDbAction: IO[Nothing, Integer] = for {
_ <- putStrLn("Calling the database now")
} yield 42
def maybeUseTheValue(x: IO[Nothing, Integer]): IO[Nothing, Unit] = for {
gettingItNow <- x
_ <- putStrLn(s"The database said ${gettingItNow}")
} yield ()
def maybeNeedItAgain(x: IO[Nothing, Integer]): IO[Nothing, Unit] = for {
gettingItNow <- x
_ <- putStrLn(s"Okay, we need it again here: ${gettingItNow}")
} yield ()
override def run(args: List[String]): IO[Nothing, Main.ExitStatus] = for {
_ <- maybeUseTheValue(longRunningDbAction)
_ <- maybeNeedItAgain(longRunningDbAction)
} yield ExitStatus.ExitNow(0)
}
Is there a way to "wrap" the longRunningDbAction
into something that makes it lazy?
回答1:
I came up with the following:
def lazyIO[E,A](io: IO[E,A]): IO[Nothing, IO[E, A]] = {
for {
barrier <- Promise.make[Nothing, Unit]
fiber <- (barrier.get *> io).fork
} yield barrier.complete(()) *> putStrLn("getting it") *> fiber.join
}
Updated version for ZIO 1.0-RC4 (with Environment support):
def lazyIO[R, E, A](io: ZIO[R, E, A]): ZIO[R, Nothing, ZIO[R, E, A]] = {
for {
barrier <- Promise.make[Nothing, Unit]
fiber <- (barrier.await *> io).fork
} yield barrier.succeed(()) *> fiber.join
}
So this is an IO that takes an IO and returns a lazy version of it.
It works by starting a fiber
that runs the original io
, but only after a Promise (barrier
) has been completed.
The lazy IO first completes that barrier
(which if it was the first one to do this will unblock the fiber
which in turn runs the wrapped io
) and then joins the fiber
to retrieve the calculation result.
With this, I can do
override def run(args: List[String]): IO[Nothing, Main.ExitStatus] = for {
valueFromDb <- lazyIO(longRunningDbAction)
_ <- maybeUseTheValue(valueFromDb)
_ <- maybeNeedItAgain(valueFromDb)
} yield ExitStatus.ExitNow(0)
And the console output shows that indeed the lazy value gets pulled twice but only the first one triggers the "database access":
getting it
Calling the database now
The database said 42
getting it
Okay, we need it again here: 42
回答2:
ZIO has memoize now.
override def run(args: List[String]): IO[Nothing, Main.ExitStatus] = for {
valueFromDb <- ZIO.memoize(longRunningDbAction)
_ <- maybeUseTheValue(valueFromDb)
_ <- maybeNeedItAgain(valueFromDb)
} yield ExitStatus.ExitNow(0)
It does essentially the same thing as this answer: Source looks like this
/**
* Returns an effect that, if evaluated, will return the lazily computed result
* of this effect.
*/
final def memoize: ZIO[R, Nothing, IO[E, A]] =
for {
r <- ZIO.environment[R]
p <- Promise.make[E, A]
l <- Promise.make[Nothing, Unit]
_ <- (l.await *> ((self provide r) to p)).fork
} yield l.succeed(()) *> p.await
来源:https://stackoverflow.com/questions/53851349/how-do-i-make-a-scalaz-zio-lazy