3

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?

Krzysztof Atłasik
  • 21,985
  • 6
  • 54
  • 76
Thilo
  • 257,207
  • 101
  • 511
  • 656
  • What determines if you need to call the DB again? Is it something that is determined in the first call out? – user3056052 Jul 09 '19 at 03:45
  • @user3056052 It is supposed to work like a regular `lazy val dbResult = callDatabase()` in Scala, i.e. be evaluated the first time it is called and afterwards just return the same value again (without doing anything else). – Thilo Jul 09 '19 at 09:01

2 Answers2

4

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
Thilo
  • 257,207
  • 101
  • 511
  • 656
  • Why not add `).flatten` at the end to avoid the ZIO or ZIO as return type ? And it did not seem to work for me it is being call several times – Wonay Jul 09 '19 at 00:20
  • @Wonay: I think it has to be a ZIO of a ZIO, because it is an effectful function (creating a promise) that produces another effectful function (which will complete the promise). But I might be wrong. Can you post the code that does not work for you somewhere? – Thilo Jul 09 '19 at 00:41
  • I call the `LazyIO(...)` in two for-comprehension and it is executed twice. I posted https://stackoverflow.com/questions/56944089/zio-how-to-compute-only-once – Wonay Jul 09 '19 at 17:21
  • Yes, that does not work. You are supposed to call `lazyIO` only once, which gives you another ZIO that you can then call repeatedly. That's what I meant by not being able to flatten this. I have to look at the `memoize` thing mentioned in that thread. – Thilo Jul 10 '19 at 05:37
  • Note that this `memoize` also has the same kind of ZIO of ZIO signature. – Thilo Jul 10 '19 at 05:39
3

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

Thilo
  • 257,207
  • 101
  • 511
  • 656
  • The main difference to the other answer is that it also captured the resource, so that using the lazy value does not need the `R` (only creating it does). – Thilo Jul 15 '19 at 09:06