关于haskell:这个斐波纳契函数如何记忆?

How is this fibonacci-function memoized?

这个斐波那契函数是通过什么机制记忆的?

1
2
3
4
fib = (map fib' [0..] !!)                
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)

在相关的注释中,为什么这个版本不是?

1
2
3
4
fib n = (map fib' [0..] !! n)                                              
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)


haskell中的评估机制是按需进行的:当需要一个值时,它会被计算出来,并在需要时随时准备好。如果我们定义了某个列表,xs=[0..],然后要求它的第100个元素xs!!99,那么列表中的第100个槽将被"充实",现在保留数字99,准备好下一次访问。

这就是"通过名单"的伎俩所利用的。在正常的双递归Fibonacci定义中,fib n = fib (n-1) + fib (n-2)调用函数本身,从顶部调用两次,导致指数爆炸。但有了这个技巧,我们列出了一个临时结果的列表,并"浏览列表":

1
fib n = (xs!!(n-1)) + (xs!!(n-2)) where xs = 0:1:map fib [2..]

诀窍是使该列表被创建,并使该列表在对fib的调用之间不会消失(通过垃圾收集)。要做到这一点,最简单的方法就是列出清单。"如果你说出它的名字,它就会一直存在。"

第一个版本定义了一个单态常量,第二个版本定义了一个多态函数。多态函数不能对它可能需要服务的不同类型使用相同的内部列表,因此不需要共享,即不需要记忆。

在第一个版本中,编译器对我们很慷慨,去掉了常量子表达式(map fib' [0..]),使它成为一个单独的可共享实体,但它没有义务这样做。实际上,有些情况下我们不希望它自动为我们做这些。

(编辑:)考虑这些重新写入:

1
2
3
4
5
6
7
fib1 = f                     fib2 n = f n                 fib3 n = f n          
 where                        where                        where                
  f i = xs !! i                f i = xs !! i                f i = xs !! i      
  xs = map fib' [0..]          xs = map fib' [0..]          xs = map fib' [0..]
  fib' 1 = 1                   fib' 1 = 1                   fib' 1 = 1          
  fib' 2 = 1                   fib' 2 = 1                   fib' 2 = 1          
  fib' i=fib1(i-2)+fib1(i-1)   fib' i=fib2(i-2)+fib2(i-1)   fib' i=f(i-2)+f(i-1)

因此,实际情况似乎是关于嵌套的范围定义。第1个定义没有外部作用域,第3个定义小心不要调用外部作用域fib3,而是调用同一级别的f

每次新调用fib2似乎都会重新创建其嵌套定义,因为它们中的任何一个(理论上)都可以根据n的值进行不同的定义(感谢vitus和tikhon指出这一点)。第一个定义没有依赖的n,第三个定义有依赖关系,但是对fib3的每个单独调用都调用f,它小心地只调用来自同一级别范围的定义,内部调用fib3,因此同一个xs被重用(即共享)给t。调用fib3

但是,没有什么能阻止编译器认识到上述任何版本中的内部定义实际上独立于外部n绑定,从而执行lambda提升,从而导致完全的memoization(多态定义除外)。事实上,当用单态类型声明并用-o2标志编译时,这正是所有三个版本所发生的事情。在多态类型声明中,fib3显示出局部共享,fib2根本不共享。

最终,取决于使用的编译器和编译器优化,以及如何测试它(在ghci中加载文件,是否编译,是否使用-o2,或独立),以及它是否是单态或多态类型,行为可能会完全改变-是否显示局部(每次调用)共享(即每次调用的线性时间),记忆化(即,第一次调用的线性时间,具有相同或较小参数的后续调用的0次),或者根本不共享(指数时间)。

简短的回答是,这是一个编译器的东西。:)


我不完全确定,但这里有一个有根据的猜测:

编译器假设fib n在不同的n上可能不同,因此每次都需要重新计算列表。毕竟,where语句中的位可以依赖于n。也就是说,在这种情况下,整个数字列表本质上是n的函数。

没有n的版本可以创建一次列表并将其包装在函数中。列表不能依赖于传入的n的值,这很容易验证。该列表是一个常量,然后被编入索引。当然,它是一个延迟计算的常量,因此您的程序不会立即尝试获取整个(无限)列表。因为它是一个常量,所以可以在函数调用之间共享。

它完全是内存化的,因为递归调用只需要在列表中查找一个值。由于fib版本创建列表的时候很懒惰,所以它只需要计算足够的数据就可以得到答案,而不需要做多余的计算。这里,"lazy"意味着列表中的每个条目都是thunk(一个未计算值的表达式)。当您计算thunk时,它会变成一个值,所以下次访问它时不会重复计算。由于列表可以在两个调用之间共享,因此所有以前的条目都已按您需要下一个条目的时间计算。

它本质上是一种基于GHC懒惰语义的智能、低成本的动态编程形式。我认为该标准只指定它必须是非严格的,因此兼容的编译器可能会将此代码编译为非memoize。然而,在实践中,每一个合理的编译器都是懒惰的。

有关第二种情况的工作原理的更多信息,请阅读了解递归定义的列表(fibs中的zipWith)。


首先,使用ghc-7.4.2,用-O2编译,非Memoised版本没有那么差,对于函数的每个顶级调用,fibonacci编号的内部列表仍然是Memoised。但是,不同的顶级电话之间并没有,也不能合理地相互交流。但是,对于另一个版本,列表在调用之间共享。

这是由于单态限制。

第一个是由一个简单的模式绑定(只有名称,没有参数)绑定的,因此通过单态限制,它必须得到一个单态类型。推断类型为

1
fib :: (Num n) => Int -> n

并且这样的约束(在没有默认声明的情况下)被默认到Integer,将类型固定为

1
fib :: Int -> Integer

因此,只有一个列表(类型为[Integer])用于Memoise。

第二个是用一个函数参数定义的,因此它仍然是多态的,如果内部列表是跨调用的Memoised,则必须为Num中的每种类型保留一个列表。这不现实。

在禁用单态限制或具有相同类型签名的情况下编译这两个版本,并且两个版本的行为完全相同。(对于旧的编译器版本,这不是真的,我不知道是哪个版本先做的。)


你不需要对haskell使用memoize函数。只有经验编程语言才需要这些函数。然而,哈斯克尔是功能语言和…

所以,这是非常快速的斐波那契算法的例子:

1
fib = zipWith (+) (0:(1:fib)) (1:fib)

ZipWith是标准序曲的功能:

1
2
3
zipWith :: (a->b->c) -> [a]->[b]->[c]
zipWith op (n1:val1) (n2:val2) = (n1 + n2) : (zipWith op val1 val2)
zipWith _ _ _ = []

测试:

1
print $ take 100 fib

输出:

1
[1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,165580141,267914296,433494437,701408733,1134903170,1836311903,2971215073,4807526976,7778742049,12586269025,20365011074,32951280099,53316291173,86267571272,139583862445,225851433717,365435296162,591286729879,956722026041,1548008755920,2504730781961,4052739537881,6557470319842,10610209857723,17167680177565,27777890035288,44945570212853,72723460248141,117669030460994,190392490709135,308061521170129,498454011879264,806515533049393,1304969544928657,2111485077978050,3416454622906707,5527939700884757,8944394323791464,14472334024676221,23416728348467685,37889062373143906,61305790721611591,99194853094755497,160500643816367088,259695496911122585,420196140727489673,679891637638612258,1100087778366101931,1779979416004714189,2880067194370816120,4660046610375530309,7540113804746346429,12200160415121876738,19740274219868223167,31940434634990099905,51680708854858323072,83621143489848422977,135301852344706746049,218922995834555169026,354224848179261915075,573147844013817084101]

经过时间:0.00018s