【问题标题】:Confused about cats-effect Async.memoize对猫效应 Async.memoize 感到困惑
【发布时间】:2020-10-25 05:42:02
【问题描述】:

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

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

懒惰地记住 f。对于每次返回的 F[F[A]] 被绑定, 效果 f 最多执行一次(当内部 F[A] 被绑定时 第一次)。

我对 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 存储到地图之前将其展平,但这并没有多大作用)。

【问题讨论】:

    标签: scala scala-cats cats-effect


    【解决方案1】:

    考虑以下示例

    给定以下辅助方法

    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 正在工作。

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

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

    它输出plus1 两次,因为io1io2 分别被记忆。

    对于您的示例,foo 打印一次,因为您使用的是地图并在未找到时更新该值,并且这只发生一次。每次评估IO 时都会打印bar,因为调用res.flatten 会失去记忆效果。

    【讨论】:

    • 嗯,谢谢你的回答。鉴于您只能使用一次 memoized 函数的限制,我想不出任何可以想象的用途。但现在我知道了。
    • 我相信这里更有用的方法是使用 Ref[IO,Map[...]] 或 MVar[...]。这几乎具有我正在寻找的功能。
    猜你喜欢
    • 1970-01-01
    • 2019-09-13
    • 2012-07-22
    • 2013-05-13
    • 2020-04-16
    • 2023-03-08
    • 2019-08-04
    • 2019-12-30
    • 2022-01-20
    相关资源
    最近更新 更多