斐波那契功能如何记忆?


114

斐波那契功能是通过什么机制记忆的?

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

与此相关的是,为什么没有这个版本?

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

13
一点无关地,fib 0它不会终止:您可能希望基本情况fib'fib' 0 = 0fib' 1 = 1
休恩

1
请注意,第一个版本可能更简洁:fibs = 1:1:zipWith (+) fibs (tail fibs)fib = (fibs !!)
巴斯蒂安2012年

Answers:


95

Haskell中的评估机制是必要的:当需要一个值时,将对其进行计算,并准备好以防再次请求。如果我们定义一些列表,xs=[0..]然后再要求其第100个元素,xs!!99则列表中的第100个插槽将被“清除”,99现在保留该数字,以备下次访问。

这就是“遍历列表”的窍门。在正常的双递归斐波那契定义中,fib n = fib (n-1) + fib (n-2)函数本身从顶部两次被调用,从而导致指数爆炸。但是有了这个技巧,我们为中期结果列出了一个清单,然后“遍历清单”:

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

技巧是使该列表被创建,并使该列表在调用之间不消失(通过垃圾回收)fib。实现此目的的最简单方法是命名该列表。“如果您命名,它将保留下去。”


您的第一个版本定义了一个单态常量,第二个版本定义了一个多态函数。多态函数不能针对可能需要使用的不同类型使用相同的内部列表,因此,无需共享即没有便笺。

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

编辑:)考虑以下重写:

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)

因此,真实的故事似乎与嵌套作用域定义有关。第一个定义没有外部作用域,第三个定义注意不要叫外部作用域fib3,而是同级f

每一个新的调用fib2似乎重新创建,因为它们中的任何其嵌套定义可以(在理论上)作出不同的定义取决于上的价值n(感谢维斯特和吉洪指出了这一点)。与第一确定指标没有n依靠,并与第三是有相关性,但每个单独的呼叫fib3通话将f其谨慎地从同一级别的范围仅调用定义,内部的这种特定的调用fib3,所以同样xs得到在的调用中重复使用(即共享)fib3

但是,没有什么可以阻止编译器认识到上述任何版本中的内部定义实际上都与外部绑定无关,毕竟n执行lambda提升会产生完整的记忆(多态定义除外)。实际上,当使用单态类型声明并使用-O2标志进行编译时,这三个版本都会发生这种情况。使用多态类型声明时,fib3显示局部共享,而fib2根本不共享。

最终,取决于编译器和使用的编译器优化,以及测试方式(将文件加载到GHCI中,是否使用-O2进行编译或不进行编译,还是独立运行),以及该行为是否为单态或多态类型,行为可能完全更改-无论是本地共享(每次通话)共享(即每次通话的线性时间),记忆(即首次通话的线性时间,参数相同或较小的后续通话的零时间),还是完全不共享(指数时间)。

简短的答案是,这是编译器。:)


4
只是为了解决一点细节:第二个版本没有任何共享,主要是因为fib'每个函数都重新定义了本地函数n,因此fib'fib 1fib'in fib 2中也是如此,这也意味着列表是不同的。即使您将类型固定为单态,它仍会表现出这种行为。
Vitus 2012年

1
where子句引入共享的方式很像let表达式,但是它们倾向于隐藏诸如此类的问题。更明确地重写它,您将得到:hpaste.org/71406
Vitus

1
关于重写的另一个有趣的要点:如果给它们单态类型(即Int -> Integer),则以fib2指数时间运行,fib1并且fib3都以线性时间运行,但fib1也会被记住-再次是因为fib3对于每个,都会重新定义局部定义n
Vitus

1
@misterbee但是,确实可以从编译器中获得某种保证;对特定实体的内存驻留方式的某种控制。有时我们想要共享,有时我们想要阻止它。我想象/希望应该有可能...
尼斯2014年

1
@ElizaBrandt我的意思是,有时我们想重新计算一些繁重的内容,以至于我们不将其保留在内存中,即重新计算的成本低于保持巨大内存的成本。一个示例是powerset创建:在pwr (x:xs) = pwr xs ++ map (x:) pwr xs ; pwr [] = [[]]我们中,我们希望pwr xs独立计算两次,因此可以在生产和消费时即时收集垃圾。
尼斯,

23

我不确定,但这是有根据的猜测:

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

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

完全记住它是因为递归调用只需要在列表中查找一个值。由于该fib版本一次懒惰地创建列表,因此它只需进行足够的计算即可获得答案,而无需进行多余的计算。在这里,“懒惰”表示列表中的每个条目都是一个thunk(未评估的表达式)。当你评估形实转换,就变成了价值,所以下一次就没有重复计算访问它。由于可以在呼叫之间共享该列表,因此,在需要下一个条目时,所有先前的条目都已计算出来。

从本质上讲,它是基于GHC的惰性语义的一种聪明而廉价的动态编程形式。我认为该标准仅指定必须是非严格的,因此兼容的编译器可能会编译该代码以使其不成为回忆。但是,实际上,每个合理的编译器都会变得很懒。

有关为什么第二种情况根本无法工作的更多信息,请阅读了解递归定义的列表(以zipWith表示的文件)


您是说“ fib' n在不同的地方可能有所不同n”吗?
内斯

我想我不是很清楚:我的意思是里面的所有东西fib,包括fib'每个东西,可能都不同n。我认为原始示例有些令人困惑,因为它fib'也取决于它自己的n影子n
蒂洪·耶维斯

20

首先,使用ghc-7.4.2(使用编译)时-O2,非记忆性版本还不错,对于该函数的每个顶级调用,斐波那契数字的内部列表仍然会被记住。但是,它不能也不能合理地在不同的顶级调用中存储。但是,对于其他版本,该列表在呼叫之间共享。

那是由于单态性限制。

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

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

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

fib :: Int -> Integer

因此,只有一个列表(类型为[Integer])要记住。

第二个函数是用函数参数定义的,因此它保持多态性,如果内部列表是在调用之间存储的,则必须为中的每种类型存储一个列表Num。那不切实际。

编译禁用单态性限制或具有相同类型签名的两个版本,并且两者表现出完全相同的行为。(对于较早的编译器版本而言并非如此,我不知道首先使用哪个版本。)


记住每种类型的列表为什么不切实际?原则上,GHC是否可以创建字典(类似于调用受类型类约束的函数)以包含运行时遇到的每种Num类型的部分计算列表?
misterbee 2014年

1
@misterbee原则上可以,但是如果程序调用fib 1000000很多类型,则会消耗大量内存。为了避免这种情况,需要一种启发式的方法,当它变得太大时,该列表会从缓存中抛出。而且,这种记忆策略也可能适用于其他函数或值,因此,编译器将不得不处理大量潜在事物,以便为多种类型记忆。我认为可以通过合理的启发式实现(部分)多态记忆,但是我怀疑这样做是否值得。
Daniel Fischer

5

您不需要Haskell的备忘功能。只有经验性编程语言才需要该功能。但是,Haskel是功能性语言,并且...

因此,这是非常快速的斐波那契算法的示例:

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

zipWith是标准Prelude中的函数:

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

测试:

print $ take 100 fib

输出:

[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


这个解决方案很棒!
拉里
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.