例如,说这是我的功能:
let fib = n => {
switch (n) {
case 0: return 0
case 1: return 1
default: return fib(n - 1) + fib(n - 2)
}
}
然后我可以实现一个基本的memoize函数......
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
时它都会返回[value,fib]元组,其中fib
现在有缓存中的某些内容,留下原始{{1}未经修改。
monad方法的缺点是它不是惯用的JavaScript,并且在没有静态类型系统的情况下很难使用。
是否有其他方法可以实现避免变异的fib
函数,而无需使用monad?
答案 0 :(得分:2)
首先,即使在所有合理标准都是“纯粹”的Haskell中,在评估之后,thunk在内部被其结果覆盖 - 阅读graph reduction。这是纯度和效率之间的权衡,但是它会隐藏用户的杂质,从而使其成为一个实现细节。
但你的问题的答案是肯定的。考虑一下你在纯粹的命令式设置中所做的事情:动态编程。您将考虑如何将函数构造为表查找,然后从下到上构建该表。现在,许多人应用这个就是建立一个memoized递归函数。
你可以改变原理,并且在函数式语言中使用“自下而上的表技巧”来获得一个memoized递归函数,只需以纯粹的方式构建查找表:
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?。