【问题标题】:How do I write a 100% pure memoize function?如何编写 100% 纯 memoize 函数?
【发布时间】:2017-11-03 15:31:41
【问题描述】:

例如,说这是我的功能:

let fib = n => {
  switch (n) {
    case 0: return 0
    case 1: return 1
    default: return fib(n - 1) + fib(n - 2)
  }
}

然后我可以实现一个基本的记忆功能...

let memoize = fn => {
  let cache = new Map   // mutates!
  return _ => {
    if (!cache.has(_)) {
      cache.set(_, fn(_))
    }
    return cache.get(_)
  }
}

...并用它来实现一个记忆化的fib:

let fib2 = memoize(n => {
  switch (n) {
    case 0: return 0
    case 1: return 1
    default: return fib2(n - 1) + fib2(n - 2)
  }
})

但是,memoize 函数在内部使用可变状态。我可以将memoize 重新实现为一个monad,所以每次我们调用fib 时它都会返回一个[value, fib] 的元组,其中fib 现在在缓存中有一些东西,而原始的fib 保持不变。

monad 方法的缺点是它不是惯用的 JavaScript,而且如果没有静态类型系统就很难使用。

是否有其他方法可以实现 memoize 函数,避免突变,而不求助于单子?

【问题讨论】:

  • 考虑到给定的答案,如果 Javascript 有一个非严格的评估策略是可能的

标签: javascript types functional-programming computer-science monads


【解决方案1】:

首先,即使在所有合理标准下都是“纯”的 Haskell 中,thunk 也会在内部被评估后的结果覆盖——请阅读graph reduction。这是纯度和效率之间的权衡,但它对用户隐藏了杂质,因此使其成为实现细节。

但你的问题的答案是肯定的。考虑一下您在纯命令式设置中会做什么:动态编程。您会考虑如何将函数构造为表查找,然后自下而上构建该表。现在,很多人在应用它是因为它只是在构建一个记忆递归函数。

您可以扭转原理,在函数式语言中使用“自下而上的表格技巧”来获得记忆递归函数,只需以纯粹的方式构建查找表:

fib n = fibs !! n
  where fibs = 0:1:zipWith (+) fibs (tail fibs)

翻译成一个懒惰的-JS:

let fibs = [0, 1, Array.zipWith((a, b) => a + b, fibs, fibs.tail)...]
let fib = (n) => fibs[n]

在 Haskell 中这是默认的,因为它是惰性的。在 JavaScript 中,您可能可以通过使用惰性流来做类似的事情。比较以下 Scala 变体:

def fibonacci(n: Int): Stream[Int] = {
   lazy val fib: Stream[Int] = 0 #:: fib.scan(1)(_+_)
   fib.take(n)
}

scan 类似于 reduce,但保持中间累积;#:: 是流的缺点;lazy val 值本质上是一个记忆 thunk。)

更多关于fib在图约简存在下的实现的思考,见How is this fibonacci-function memoized?

【讨论】:

  • 啊,我一直想知道 Haskell 如何实现评估 thunk 的结果共享。现在,这感觉像是在作弊。
  • 嗯,这是一个非常优雅的分离。您拥有纯图形,然后可以将它们编译为用于评估它们的abstract machine——可变的,但一种“ML 语言的汇编语言”。它也比“覆盖结果”更微妙。到目前为止,我还不是专家。
  • 这是有道理的,但在 JS 中类似于我示例中的 hashmap-building 方法,因为数组是可变的。 Node 内置的 Streams 类只能获取流的最新元素,因此我们必须自己跟踪已经看到的元素。听起来您的方法的重点是它仍然是可变的,但是这种可变性不会暴露给程序员;因为 JS 没有惰性 eval,所以我们需要在应用层跟踪状态。
  • @bcherny 我想我在不同意义上使用“流”——本质上,一个惰性的、可能是无限的列表,它可以有一个递归定义。这样的事情可以用一种急切的语言来实现,使用闭包。请参阅我的 Scala 示例——尽管考虑到 lazy vals 是如何实现的,但您的最后一句话是正确的。
  • 现在在 NPM 上发布为“lazy-arr” - npmjs.com/package/lazy-arr
猜你喜欢
  • 2010-09-12
  • 2010-12-17
  • 2014-06-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多