对猫效应感到困惑 Async.memoize

Confused about cats-effect Async.memoize

我对猫效应还很陌生,但我想我已经掌握了它。但是我遇到了一种情况,我想记住 IO 的结果,但它并没有按照我的预期进行。

我要记忆的函数转换String => String,但是转换需要网络调用,所以实现为函数String => IO[String]。在非 IO 世界中,我会简单地保存调用的结果,但定义函数实际上无法访问它,因为它直到稍后才执行。如果我保存构造的 IO[String],它实际上不会有帮助,因为每次使用时 IO 都会重复网络调用。因此,我尝试使用 Async.memoize,它具有以下文档:

Lazily memoizes f. For every time the returned F[F[A]] is bound, the effect f will be performed at most once (when the inner F[A] is bound the first time).

我对 memoize 的期望是一个只对给定输入执行一次的函数,并且返回的 IO 的内容只被计算一次;换句话说,我希望生成的 IO 就像 IO.pure(result) 一样,除了第一次。但这似乎不是正在发生的事情。相反,我发现虽然被调用的函数本身只执行一次,但 IO 的内容仍然每次都会被评估——就像我试图天真地保存和重用 IO 时会发生的那样。

我构造了一个例子来说明问题:

def plus1(num: Int): IO[Int] = {
      println("foo")
      IO(println("bar")) *> IO(num + 1)
    }
    var fooMap = Map[Int, IO[IO[Int]]]()
    def mplus1(num: Int): IO[Int] = {
      val check = fooMap.get(num)
      val res = check.getOrElse {
        val plus = Async.memoize(plus1(num))
        fooMap = fooMap + ((num, plus))
        plus
      }
      res.flatten
    }

    println("start")
    val call1 = mplus1(2)
    val call2 = mplus1(2)
    val result = (call1 *> call2).unsafeRunSync()
    println(result)
    println(fooMap.toString)
    println("finish")

这个程序的输出是:

start
foo
bar
bar
3
Map(2 -> <function1>)
finish

虽然 plus1 函数本身只执行一次(打印一个“foo”),但 IO 中包含的输出“bar”打印了两次,而我希望它也只打印一次。 (我也试过在将 Async.memoize 返回的 IO 存储在地图中之前将其展平,但这并没有太大作用)。

考虑以下示例

给定以下辅助方法

def plus1(num: Int): IO[IO[Int]] = {
  IO(IO(println("plus1")) *> IO(num + 1))
}

def mPlus1(num: Int): IO[IO[Int]] = {
  Async.memoize(plus1(num).flatten)
}

让我们构建一个计算 plus1(1) 两次的程序。

val program1 = for {
  io <- plus1(1)
  _ <- io
  _ <- io
} yield {}
program1.unsafeRunSync()

这会产生两次打印 plus1 的预期输出。

如果您使用 mPlus1 方法进行相同的操作

val program2 = for {
  io <- mPlus1(1)
  _ <- io
  _ <- io
} yield {}
program2.unsafeRunSync()

它将打印 plus1 一次,确认记忆正在工作。

memoization 的诀窍是它应该只被评估一次才能达到预期的效果。现在考虑以下突出显示它的程序。

val memIo = mPlus1(1)
val program3 = for {
  io1 <- memIo
  io2 <- memIo
  _ <- io1
  _ <- io2
} yield {}
program3.unsafeRunSync()

并且它输出 plus1 两次,因为 io1io2 是分开记忆的。

对于您的示例,foo 被打印一次,因为您正在使用地图并在未找到地图时更新值,而且这种情况只发生一次。每次评估 IO 时都会打印 bar,因为您通过调用 res.flatten.

失去了记忆效果