【问题标题】:How is this fibonacci-function memoized?这个斐波那契函数是如何记忆的?
【发布时间】:2023-03-11 22:16:01
【问题描述】:

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

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)                    

【问题讨论】:

  • 有点不相关,fib 0 不会终止:您可能希望fib' 的基本情况为fib' 0 = 0fib' 1 = 1
  • 请注意,第一个版本可以更简洁:fibs = 1:1:zipWith (+) fibs (tail fibs)fib = (fibs !!)

标签: haskell lazy-evaluation fibonacci memoization pointfree


【解决方案1】:

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

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

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

它完全被记住了,因为递归调用只需要在列表中查找一个值。由于fib 版本会懒惰地创建一次列表,因此它的计算量足以得到答案,而无需进行冗余计算。这里,“懒惰”意味着列表中的每个条目都是一个 thunk(一个未计算的表达式)。当您确实评估 thunk 时,它会变成一个值,因此下次访问它不会重复计算。由于列表可以在调用之间共享,所有之前的条目都已经在您需要下一个条目的时间计算出来了。

它本质上是一种基于 GHC 的惰性语义的智能且廉价的动态编程形式。我认为标准只规定它必须是non-strict,因此兼容的编译器可能会将这段代码编译为not memoize。然而,在实践中,每一个合理的编译器都会变得懒惰。

有关第二种情况为何有效的更多信息,请阅读Understanding a recursively defined list (fibs in terms of zipWith)

【讨论】:

  • 您的意思是“fib' n 在不同的n 上可能会有所不同”吗?
  • 我想我不是很清楚:我的意思是fib 中的所有内容,包括fib',在每个不同的n 上都可能不同。我认为原始示例有点令人困惑,因为fib' 还依赖于它自己的n,它会影响另一个n
【解决方案2】:

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 的每次新调用似乎都会重新创建其嵌套定义,因为它们中的任何一个都可以(理论上)根据@987654334 的值进行不同的定义 @(感谢 Vitus 和 Tikhon 指出这一点)。第一个定义没有 n 依赖,第三个有依赖关系,但是每个单独调用 fib3 调用 f 小心只调用来自同级范围的定义,内部fib3 的特定调用,因此相同的 xs 被重用(即共享)用于 fib3 的调用。

但没有什么能阻止编译器认识到上述任何版本中的内部定义实际上是独立外部n绑定,毕竟执行lambda lifting,导致完整的记忆(除了多态定义)。事实上,当使用单态类型声明并使用 -O2 标志编译时,这正是所有三个版本所发生的情况。使用多态类型声明,fib3 显示本地共享,fib2 根本没有共享。

最终,取决于编译器和使用的编译器优化,以及您如何测试它(在 GHCI 中加载文件,编译与否,是否使用 -O2,或独立),以及它是否获得单态或多态类型行为可能会完全改变——它是否表现出本地(每次调用)共享(即每次调用的线性时间)、记忆化(即第一次调用的线性时间,以及具有相同或更小参数的后续调用的 0 时间),或者不共享完全没有(指数时间)。

简短的回答是,这是编译器的事情。 :)

【讨论】:

  • 修复一个小细节:第二个版本没有得到任何共享,主要是因为本地函数fib'为每个n重新定义,因此fib'fib 1≠@ fib 2 中的 987654348@,这也意味着列表不同。即使您确实将类型固定为单态,它仍然会表现出这种行为。
  • where 子句引入了与let 表达式非常相似的共享,但它们倾向于隐藏诸如此类的问题。更明确地重写它,你会得到:hpaste.org/71406
  • 另一个关于重写的有趣点:如果你给它们单态类型(即Int -> Integer),那么fib2 以指数时间运行,fib1fib3 都以线性时间运行,但fib1 也被记住了 - 再次因为 fib3 为每个 n 重新定义本地定义。
  • @misterbee 但是,从编译器那里得到某种保证确实会很好;对特定实体的内存驻留进行某种控制。有时我们想要分享,有时我们想要阻止它。我想/希望它应该是可能的......
  • @ElizaBrandt 我的意思是有时我们想要重新计算一些很重的东西,这样它就不会为我们保留在内存中——即重新计算的成本低于保留大量内存的成本。一个例子是 powerset 创建:在 pwr (x:xs) = pwr xs ++ map (x:) pwr xs ; pwr [] = [[]] 中,我们确实希望 pwr xs 被独立计算两次,因此它可以在生产和消费时动态收集。
【解决方案3】:

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

这是由于单态限制。

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

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

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

fib :: Int -> Integer

因此只有一个列表([Integer] 类型)需要记忆。

第二个是用函数参数定义的,因此它仍然是多态的,如果内部列表在调用之间被记忆,则必须为Num 中的每种类型记忆一个列表。这不切实际。

在禁用单态限制或使用相同类型签名的情况下编译两个版本,并且都表现出完全相同的行为。 (旧的编译器版本不是这样,我不知道是哪个版本最先做到的。)

【讨论】:

  • 为什么记住每种类型的列表是不切实际的?原则上,GHC 是否可以创建一个字典(很像调用类型类约束函数)来包含运行时遇到的每个 Num 类型的部分计算列表?
  • @misterbee 原则上可以,但是如果程序在很多类型上调用fib 1000000,就会占用大量内存。为了避免这种情况,人们需要一种启发式方法,当它变得太大时,它会从缓存中抛出。而且这种记忆策略也可能适用于其他函数或值,因此编译器将不得不处理潜在的大量事物来记忆潜在的多种类型。我认为可以通过相当好的启发式实现(部分)多态记忆,但我怀疑这是否值得。
【解决方案4】:

Haskell 不需要 memoize 功能。只有经验性编程语言需要这些功能。但是,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

【讨论】:

    猜你喜欢
    • 2013-03-10
    • 2021-11-17
    • 2021-05-21
    • 1970-01-01
    • 2011-12-14
    • 2019-06-04
    • 2015-10-19
    • 2020-08-19
    • 1970-01-01
    相关资源
    最近更新 更多