【问题标题】:What type to use to store an in-memory mutable data table in Scala?在 Scala 中使用什么类型来存储内存中的可变数据表?
【发布时间】:2011-04-08 03:16:45
【问题描述】:

每次调用函数时,如果给定参数值集的结果尚未被记忆,我想将结果放入内存表中。一列用于存储结果,其他列用于存储参数值。

我如何最好地实现这一点?参数有多种类型,包括一些枚举。

在 C# 中,我通常使用 DataTable。 Scala 中是否有等价物?

【问题讨论】:

  • 如果你在网上搜索“Scala Function Memoization”,你会发现这个主题的几种处理方法。

标签: scala data-structures memoization scala-collections


【解决方案1】:

您可以使用mutable.Map[TupleN[A1, A2, ..., AN], R] ,或者如果内存是一个问题,则可以使用 Wea​​kHashMap[1]。下面的定义(基于michid's blog 的记忆代码)允许您轻松记忆具有多个参数的函数。例如:

import Memoize._

def reallySlowFn(i: Int, s: String): Int = {
   Thread.sleep(3000)
   i + s.length
}

val memoizedSlowFn = memoize(reallySlowFn _)
memoizedSlowFn(1, "abc") // returns 4 after about 3 seconds
memoizedSlowFn(1, "abc") // returns 4 almost instantly

定义:

/**
 * A memoized unary function.
 *
 * @param f A unary function to memoize
 * @param [T] the argument type
 * @param [R] the return type
 */
class Memoize1[-T, +R](f: T => R) extends (T => R) {
   import scala.collection.mutable
   // map that stores (argument, result) pairs
   private[this] val vals = mutable.Map.empty[T, R]

   // Given an argument x, 
   //   If vals contains x return vals(x).
   //   Otherwise, update vals so that vals(x) == f(x) and return f(x).
   def apply(x: T): R = vals getOrElseUpdate (x, f(x))
}

object Memoize {
   /**
    * Memoize a unary (single-argument) function.
    *
    * @param f the unary function to memoize
    */
   def memoize[T, R](f: T => R): (T => R) = new Memoize1(f)

   /**
    * Memoize a binary (two-argument) function.
    * 
    * @param f the binary function to memoize
    * 
    * This works by turning a function that takes two arguments of type
    * T1 and T2 into a function that takes a single argument of type 
    * (T1, T2), memoizing that "tupled" function, then "untupling" the
    * memoized function.
    */
   def memoize[T1, T2, R](f: (T1, T2) => R): ((T1, T2) => R) = 
      Function.untupled(memoize(f.tupled))

   /**
    * Memoize a ternary (three-argument) function.
    *
    * @param f the ternary function to memoize
    */
   def memoize[T1, T2, T3, R](f: (T1, T2, T3) => R): ((T1, T2, T3) => R) =
      Function.untupled(memoize(f.tupled))

   // ... more memoize methods for higher-arity functions ...

   /**
    * Fixed-point combinator (for memoizing recursive functions).
    */
   def Y[T, R](f: (T => R) => T => R): (T => R) = {
      lazy val yf: (T => R) = memoize(f(yf)(_))
      yf
   }
}

定点组合器 (Memoize.Y) 使得记忆递归函数成为可能:

val fib: BigInt => BigInt = {                         
   def fibRec(f: BigInt => BigInt)(n: BigInt): BigInt = {
      if (n == 0) 1 
      else if (n == 1) 1 
      else (f(n-1) + f(n-2))                           
   }                                                     
   Memoize.Y(fibRec)
}

[1] WeakHashMap 不能很好地用作缓存。请参阅http://www.codeinstructions.com/2008/09/weakhashmap-is-not-cache-understanding.htmlthis related question

【讨论】:

  • 请注意,上面的实现不是线程安全的,所以如果你需要缓存来自多个线程的一些计算,这可能会中断。为了将其更改为线程安全,只需执行以下操作: private[this] val vals = new HashMap[T, R] with SynchronizedMap[T, R]
  • 还有另一种对递归函数进行记忆的方法:stackoverflow.com/a/25129872/2073130,它不需要使用 Y 组合器或因此制定非递归形式,这对于递归函数来说可能是令人生畏的多个参数。实际上这两种方法都依赖于 Scala 自己对函数递归的支持,即当使用 Y 组合器时 yf 正在调用 yf,而在链接的 wrick 的变体中,记忆函数会调用自身。
【解决方案2】:

anovstrup 建议的使用可变 Map 的版本与 C# 中的版本基本相同,因此易于使用。

但如果您愿意,也可以使用更实用的样式。它使用不可变映射,充当一种累加器。将元组(而不是示例中的 Int)作为键的工作方式与可变情况完全相同。

def fib(n:Int) = fibM(n, Map(0->1, 1->1))._1

def fibM(n:Int, m:Map[Int,Int]):(Int,Map[Int,Int]) = m.get(n) match {
   case Some(f) => (f, m)
   case None => val (f_1,m1) = fibM(n-1,m)
                val (f_2,m2) = fibM(n-2,m1)
                val f = f_1+f_2
                (f, m2 + (n -> f))   
}

当然,这有点复杂,但却是一个有用的技巧(请注意,上面的代码是为了清晰,而不是为了速度)。

【讨论】:

    【解决方案3】:

    作为这个主题的新手,我不能完全理解给出的任何例子(但还是要感谢)。恭敬地,我会提出我自己的解决方案,以解决有人来到这里遇到相同级别和相同问题的情况。我认为我的代码对于任何只有the very-very basic Scala knowledge 的人来说都是非常清晰的。

    def MyFunction(dt : DateTime, param : Int) : Double { val argsTuple = (dt, param) if(Memo.contains(argsTuple)) Memo(argsTuple) else Memoize(dt, param, MyRawFunction(dt, param)) } def MyRawFunction(dt : DateTime, param : Int) : Double { 1.0 // A heavy calculation/querying here } def Memoize(dt : DateTime, param : Int, result : Double) : Double { Memo += (dt, param) -> result result } val Memo = new scala.collection.mutable.HashMap[(DateTime, Int), Double]

    完美运行。如果我遗漏了什么,我会很感激批评。

    【讨论】:

    • 我在我的解决方案中添加了一些 cmets,希望能为您澄清它。我概述的方法的优点是它允许您记忆 any 函数(好吧,有一些警告,但 许多函数)。有点像您在相关问题中发布的 memoize 关键字。
    • 可能仍然令人费解的一个方面是定点组合器——为此我鼓励您阅读 michid 的博客,喝大量的咖啡,也许对一些函数式编程文本感到友好。好消息是,只有在记忆递归函数时才需要它。
    【解决方案4】:

    当使用 mutable map 进行记忆时,应记住这会导致典型的并发问题,例如在写入尚未完成时执行获取。但是,内存化的线程安全尝试建议这样做,如果不是没有价值的话,它也没有什么价值。

    以下线程安全代码创建了一个记忆化的fibonacci 函数,启动了几个对其进行调用的线程(从'a' 到'd')。尝试代码几次(在 REPL 中),可以很容易地看到 f(2) set 被多次打印。这意味着线程 A 已经启动了f(2) 的计算,但线程 B 完全不知道它并开始了自己的计算副本。这种无知在缓存的构建阶段非常普遍,因为所有线程都没有看到建立的子解决方案,并且会进入else 子句。

    object ScalaMemoizationMultithread {
    
      // do not use case class as there is a mutable member here
      class Memo[-T, +R](f: T => R) extends (T => R) {
        // don't even know what would happen if immutable.Map used in a multithreading context
        private[this] val cache = new java.util.concurrent.ConcurrentHashMap[T, R]
        def apply(x: T): R =
          // no synchronized needed as there is no removal during memoization
          if (cache containsKey x) {
            Console.println(Thread.currentThread().getName() + ": f(" + x + ") get")
            cache.get(x)
          } else {
            val res = f(x)
            Console.println(Thread.currentThread().getName() + ": f(" + x + ") set")
            cache.putIfAbsent(x, res) // atomic
            res
          }
      }
    
      object Memo {
        def apply[T, R](f: T => R): T => R = new Memo(f)
    
        def Y[T, R](F: (T => R) => T => R): T => R = {
          lazy val yf: T => R = Memo(F(yf)(_))
          yf
        }
      }
    
      val fibonacci: Int => BigInt = {
        def fiboF(f: Int => BigInt)(n: Int): BigInt = {
          if (n <= 0) 1
          else if (n == 1) 1
          else f(n - 1) + f(n - 2)
        }
    
        Memo.Y(fiboF)
      }
    
      def main(args: Array[String]) = {
        ('a' to 'd').foreach(ch =>
          new Thread(new Runnable() {
            def run() {
              import scala.util.Random
              val rand = new Random
              (1 to 2).foreach(_ => {
                Thread.currentThread().setName("Thread " + ch)
                fibonacci(5)
              })
            }
          }).start)
      }
    }
    

    【讨论】:

      【解决方案5】:

      除了Landei的回答之外,我还想建议在Scala中做DP的自下而上(非记忆化)的方式是可能的,核心思想是使用foldLeft(s)。

      计算斐波那契数的示例

        def fibo(n: Int) = (1 to n).foldLeft((0, 1)) {
          (acc, i) => (acc._2, acc._1 + acc._2)
        }._1
      

      最长递增子序列示例

      def longestIncrSubseq[T](xs: List[T])(implicit ord: Ordering[T]) = {
        xs.foldLeft(List[(Int, List[T])]()) {
          (memo, x) =>
            if (memo.isEmpty) List((1, List(x)))
            else {
              val resultIfEndsAtCurr = (memo, xs).zipped map {
                (tp, y) =>
                  val len = tp._1
                  val seq = tp._2
                  if (ord.lteq(y, x)) { // current is greater than the previous end
                    (len + 1, x :: seq) // reversely recorded to avoid O(n)
                  } else {
                    (1, List(x)) // start over
                  }
              }
              memo :+ resultIfEndsAtCurr.maxBy(_._1)
            }
        }.maxBy(_._1)._2.reverse
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-02-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-02-19
        • 1970-01-01
        相关资源
        最近更新 更多