如何编写 100% 纯记忆化功能?
How do I write a 100% pure memoize function?
例如,假设这是我的函数:
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(_)
}
}
...并用它来实现一个 memoized 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
它 returns 一个 [value, fib] 的元组,其中 fib
现在在缓存中有一些东西,留下原始 fib
未修改。
monad 方法的缺点是它不是惯用的 JavaScript,如果没有静态类型系统就很难使用。
是否有其他方法可以在不求助于 monad 的情况下实现 memoize
函数来避免突变?
首先,即使在 Haskell 中,按照所有合理的标准 "pure",thunk 也会在评估后被其结果在内部覆盖——阅读有关 graph reduction 的内容。这是纯度和效率之间的权衡,但是它向用户隐藏了杂质,从而使其成为实现细节。
但是你的问题的答案是肯定的。考虑一下您在纯命令式设置中会做什么:动态编程。您会考虑如何将函数构建为 table 查找,然后自下而上构建该 table。现在,很多人应用它的是,这只是在构建一个记忆递归函数。
你可以扭转这个原则,在函数式语言中使用 "bottom-up table trick" 来获得记忆递归函数,只需以纯粹的方式构建查找 table:
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
值本质上是一个 memoizing thunk。)
要进一步思考在存在图缩减的情况下 fib
的实现,请参阅 How is this fibonacci-function memoized?。
例如,假设这是我的函数:
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(_)
}
}
...并用它来实现一个 memoized 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
它 returns 一个 [value, fib] 的元组,其中 fib
现在在缓存中有一些东西,留下原始 fib
未修改。
monad 方法的缺点是它不是惯用的 JavaScript,如果没有静态类型系统就很难使用。
是否有其他方法可以在不求助于 monad 的情况下实现 memoize
函数来避免突变?
首先,即使在 Haskell 中,按照所有合理的标准 "pure",thunk 也会在评估后被其结果在内部覆盖——阅读有关 graph reduction 的内容。这是纯度和效率之间的权衡,但是它向用户隐藏了杂质,从而使其成为实现细节。
但是你的问题的答案是肯定的。考虑一下您在纯命令式设置中会做什么:动态编程。您会考虑如何将函数构建为 table 查找,然后自下而上构建该 table。现在,很多人应用它的是,这只是在构建一个记忆递归函数。
你可以扭转这个原则,在函数式语言中使用 "bottom-up table trick" 来获得记忆递归函数,只需以纯粹的方式构建查找 table:
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
值本质上是一个 memoizing thunk。)
要进一步思考在存在图缩减的情况下 fib
的实现,请参阅 How is this fibonacci-function memoized?。