Как мне сделать Scalaz ZIO ленивым?

У меня есть тяжелая побочная функция (например, вызов базы данных), которую я хочу использовать как ленивое значение, так что она вызывается только при первом использовании (и совсем не вызывается, если никогда не использовалась).

Как мне сделать это с ZIO?

Если моя программа выглядит следующим образом, функция вызывается только один раз (но даже результат не используется вообще):

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)

}

Я полагаю, я должен пройти IO который производит Int вместо уже материализованного Int, но если я перейду в оригинале IO который просто вызывает базу данных, он будет вызываться повторно:

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)

}

Есть ли способ "обернуть" longRunningDbAction в то, что делает его ленивым?

1 ответ

Я придумал следующее:

 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
  }

Так что это IO, который берет IO и возвращает его ленивую версию.

Это работает, начиная fiber который работает оригинал io, но только после обещания (barrier) завершено.

Ленивый IO сначала завершает это barrier (который, если он сделает это первым, разблокирует fiber который в свою очередь запускает завернутый io), а затем присоединяется к fiber получить результат расчета.

С этим я могу сделать

override def run(args: List[String]): IO[Nothing, Main.ExitStatus] = for {
    valueFromDb <- lazyIO(longRunningDbAction)
    _ <- maybeUseTheValue(valueFromDb)
    _ <- maybeNeedItAgain(valueFromDb)
  } yield ExitStatus.ExitNow(0)

И вывод консоли показывает, что действительно ленивое значение извлекается дважды, но только первое вызывает "доступ к базе данных":

getting it
Calling the database now
The database said 42
getting it
Okay, we need it again here: 42

ЗИО запомнил сейчас.

override def run(args: List[String]): IO[Nothing, Main.ExitStatus] = for {
   valueFromDb <- ZIO.memoize(longRunningDbAction)
   _ <- maybeUseTheValue(valueFromDb)
   _ <- maybeNeedItAgain(valueFromDb)
} yield ExitStatus.ExitNow(0)

По сути, он делает то же самое, что и этот ответ: Источник выглядит так

/**
   * 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

Другие вопросы по тегам