【问题标题】:Scala: partially evaluate a function and cache fixed valuesScala:部分评估函数并缓存固定值
【发布时间】:2015-11-25 00:57:03
【问题描述】:

有没有一种简单的方法可以以纯函数的方式缓存部分应用函数的固定值。

代码示例:

scala> def f(x:Int,y:Int)={
    def expensiveCalculation(num:Int)={
        println("I've spent a lot of time(!) calculating square of "+num)
        num*num
    }
    lazy val x2=expensiveCalculation(x)
    lazy val y2=expensiveCalculation(y)
    lazy val r=x2+y2
    r
}

scala> def g=f(1,_:Int)

scala> g(2)
I've spent a lot of time(!) calculating square of 1
I've spent a lot of time(!) calculating square of 2
res18: Int = 5

scala> g(3)
I've spent a lot of time(!) calculating square of 1
I've spent a lot of time(!) calculating square of 3
res19: Int = 10

但我不希望为 num=1 调用两次昂贵的计算。由于昂贵的计算没有副作用,所以保存结果没有问题(请不要在我的示例代码中考虑打印输出流的副作用)。

我可以通过手动保存状态来使用 OOP 风格实现我正在寻找的东西(尽管如果你想为它编写一个通用的可重用代码,它不是很干净)。

我认为在每个函数都没有副作用的 FP 中,实现这个目标应该更容易。实际上,我想不出在纯函数式语言中将其用作默认行为的严格限制(除了一些实际问题,例如缓存所需的内存量等)。

【问题讨论】:

    标签: scala lazy-evaluation currying


    【解决方案1】:

    您可以利用 Scala 同时是 OOP 和 FP 语言以及 Scala 中的函数是对象这一事实。

    object CachedFunction extends App {
    
      val f = new Function2[Int, Int, Int] {
        def expensiveCalculation(num: Int) = {
          println("I've spent a lot of time(!) calculating square of " + num)
          num * num
        }
    
        var precomputed: Map[Int, Int] = Map()
    
        def getOrUpdate(key: Int): Int =
          precomputed.get(key) match {
            case Some(v) => v
            case None =>
              val newV = expensiveCalculation(key)
              precomputed += key -> newV
              newV
          }
    
        def apply(x: Int, y: Int): Int =
          getOrUpdate(x) + getOrUpdate(y)
      }
    
      def g = f(1, _: Int)
    
      g(2)
      g(3)
      g(3)
      f(1, 2)
    }
    

    打印:

    I've spent a lot of time(!) calculating square of 1
    I've spent a lot of time(!) calculating square of 2
    I've spent a lot of time(!) calculating square of 3
    

    我已将 fdef 更改为 val - 这允许 f 成为“存储”函数的对象,而不仅仅是每次运行其整个主体的方法。在这种情况下,每次只运行apply,并保留函数对象的实例变量。其余的都是 OOPish 方式。

    虽然这对于调用者来说是不可变的,因为返回的结果不会随时间改变,但它不是线程安全的。您可能希望使用某种同步映射来存储缓存值。

    编辑: 在我写完这篇文章后,我搜索了“函数记忆”并得到了这些类似的解决方案。不过它们更通用:

    Scala Memoization: How does this Scala memo work?

    Is there a generic way to memoize in Scala?

    http://eed3si9n.com/learning-scalaz-day16

    显然 Scalaz 中还有一些东西 :)

    编辑:

    问题在于,即使函数被部分应用或柯里化,Scala 也不会急切地评估函数的参数。它只是存储参数的值。这是一个例子:

    object CachedArg extends App {
    
      def expensiveCalculation(num: Int) = {
        println("I've spent a lot of time(!) calculating square of " + num)
        num * num
      }
    
      val ff: Int => Int => Int = a => b => expensiveCalculation(a) + expensiveCalculation(b)
      val f1 = ff(1) // prints nothing
    
      val e1 = expensiveCalculation(1) // prints for 1
      val f: (Int, Int) => Int = _ + expensiveCalculation(_)
      val g1 = f(e1, _: Int)
      g1(2) // does not recalculate for 1 obviously
      g1(3)
    }
    

    打印:

    I've spent a lot of time(!) calculating square of 1
    I've spent a lot of time(!) calculating square of 2
    I've spent a lot of time(!) calculating square of 3
    

    这表明您仍然可以手动评估一次参数并通过将其部分应用于函数(或柯里化)来“保存”它。我想这就是你所追求的。为了有更方便的方式,您可以使用这种方法:

    object CachedFunction extends App {
    
      val f = new Function1[Int, Int => Int] {
        def expensiveCalculation(num: Int) = {
          println("I've spent a lot of time(!) calculating square of " + num)
          num * num
        }
    
        def apply(x: Int) =
          new Function[Int, Int] {
            val xe = expensiveCalculation(x)
    
            def apply(y: Int) = xe + expensiveCalculation(y)
          }
      }
    
      val g1 = f(1) // prints here for eval of 1
      g1(2)
      g1(3)
    }
    

    打印:

    I've spent a lot of time(!) calculating square of 1
    I've spent a lot of time(!) calculating square of 2
    I've spent a lot of time(!) calculating square of 3
    

    然而,在最后两个例子中,memoizaition 是函数对象的本地化。您必须重用相同的函数对象才能使其工作。与此不同的是,在第一个示例中,memoizaition 对于定义函数的范围是全局的。

    【讨论】:

    • 感谢您的回答。我知道这种非 FP 方式。我正在寻找一种更通用、更实用的方法。但是您提供的链接似乎很有希望,特别是 scalaz 中的链接。我会检查他们。谷歌搜索时,我没有想到“功能记忆”这个词。谢谢。
    • Scalaz 在幕后使用了相同的方法:github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/…。最终,您需要将该状态存储在某处或传递它。
    • 你是对的。读完之后,我明白这并不是我第一眼所想的那样。我希望找到一种方法,就像 Scala 内部如何处理惰性值一样。实际上现在我看到我的案例是“功能记忆”的一个非常特殊的案例。当部分应用函数时,我们可以访问绑定到它的实例的已定义变量,现在函数的某些部分是常量。所以我认为应该可以在不定义辅助 Map 和计算任何散列函数的情况下做到这一点。稍后我将尝试编写一个肮脏的 OO 代码来实现这一点,以澄清我的观点。
    【解决方案2】:

    一个简单的实现:

    lazy val _f=  scala.collection.mutable.Map[Int, Int]()
    def f(x: Int)(y: Int) = {
      def expensiveCalculation(num: Int) = {
        println("I've spent a lot of time(!) calculating square of " + num)
        num * num
      }
      def _cached(a:Int) =_f.getOrElseUpdate(a, expensiveCalculation(a))
    
      lazy val r = _cached(x)+_cached(y)
      r
    }
    val g=f(1)_
    g(2)
    g(3)
    

    【讨论】:

    • 谢谢。您的回答比 Aleksey Izmailov 的回答更简洁。但我正在寻找一种更通用的方法,对函数的内部代码进行最少的修改。也许 scalaz Memo 类是我正在寻找的东西。
    【解决方案3】:

    我一定遗漏了一些东西,但为什么简单的闭包对你不起作用?

    scala> def f(x:Int): Int => Int ={
         |       
         |       def expensiveCalculation(num:Int)={
         |         println("I've spent a lot of time(!) calculating square of "+num)
         |         num*num
         |       }
         |       val cached=expensiveCalculation(x)
         |     
         |       def add(num: Int) = {
         |         cached + num
         |       }
         |       
         |       add
         |       
         |     }
    f: (x: Int)Int => Int
    
    scala> val g = f(1)
    I've spent a lot of time(!) calculating square of 1
    g: Int => Int = <function1>
    
    scala> g(2)
    res0: Int = 3
    
    scala> g(3)
    res1: Int = 4
    
    scala> g(4)
    res2: Int = 5
    
    scala> 
    

    【讨论】:

      猜你喜欢
      • 2017-04-06
      • 2015-11-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-03-23
      • 1970-01-01
      • 1970-01-01
      • 2017-12-28
      相关资源
      最近更新 更多