Haskell中无限列表项的惰性求值

Haskell中无限列表项的惰性求值,haskell,lazy-evaluation,Haskell,Lazy Evaluation,我对无限列表的运行时性能很好奇,比如 下表: fibs = 1 : 1 : zipWith (+) fibs (tail fibs) 这将创建一个无限的斐波那契序列列表 我的问题是,如果我做到以下几点: takeWhile (<5) fibs 1 : 1 : zipWith (+) [(1, 2), (2, 3)] => 1 : 1 : 3 : 5 现在,一旦takeWhile想要评估(当在Haskell中评估某个对象时,只要它被相同的名称引用,它就会保持评估状态1 在以下代码

我对无限列表的运行时性能很好奇,比如 下表:

fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
这将创建一个无限的斐波那契序列列表

我的问题是,如果我做到以下几点:

takeWhile (<5) fibs
1 : 1 : zipWith (+) [(1, 2), (2, 3)] => 1 : 1 : 3 : 5

现在,一旦takeWhile想要评估
(当在Haskell中评估某个对象时,只要它被相同的名称引用,它就会保持评估状态1

在以下代码中,列表
l
只计算一次(这可能很明显):

即使部分评估了某些内容,该部分仍保持评估状态:

let l = [1..10]
print $ take 5 l -- Evaluates l to [1, 2, 3, 4, 5, _]
print l          -- 1 to 5 is already evaluated; only evaluates 6..10

在您的示例中,当计算
fibs
列表中的一个元素时,它将保持计算状态。由于
zipWith
的参数引用实际的
fibs
列表,这意味着压缩表达式在计算列表中的下一个元素时将使用已经部分计算的
fibs
列表。这意味着t任何元素都不会被计算两次

1这当然不是语言语义学的严格要求,但在实践中总是如此。

插图:

module TraceFibs where

import Debug.Trace

fibs :: [Integer]
fibs = 0 : 1 : zipWith tadd fibs (tail fibs)
  where
    tadd x y = let s = x+y
               in trace ("Adding " ++ show x ++ " and " ++ show y
                                   ++ "to obtain " ++ show s)
                        s
产生

*TraceFibs> fibs !! 5
Adding 0 and 1 to obtain 1
Adding 1 and 1 to obtain 2
Adding 1 and 2 to obtain 3
Adding 2 and 3 to obtain 5
5
*TraceFibs> fibs !! 5
5
*TraceFibs> fibs !! 6
Adding 3 and 5 to obtain 8
8
*TraceFibs> fibs !! 16
Adding 5 and 8 to obtain 13
Adding 8 and 13 to obtain 21
Adding 13 and 21 to obtain 34
Adding 21 and 34 to obtain 55
Adding 34 and 55 to obtain 89
Adding 55 and 89 to obtain 144
Adding 89 and 144 to obtain 233
Adding 144 and 233 to obtain 377
Adding 233 and 377 to obtain 610
Adding 377 and 610 to obtain 987
987
*TraceFibs>

这是一个自引用的惰性数据结构,其中结构的“后面”部分按名称引用前面的部分

最初,结构只是一个计算,未计算的指针返回到它自己。当它展开时,会在结构中创建值。以后对已计算的结构部分的引用能够找到已经在那里等待它们的值。无需重新计算片段,也无需做额外的工作

内存中的结构开始时只是一个未赋值的指针。当我们查看第一个值时,它如下所示:

> take 2 fibs

(一个指向cons单元格的指针,指向“1”,一个尾部包含第二个“1”,一个指向函数的指针,该函数包含对fibs的引用,以及fibs的尾部

评估另一个步骤将扩展结构,并滑动参考:

因此,我们展开结构,每次都产生一个新的未评估的尾部,这是一个闭包,将引用保留到最后一步的第一和第二个元素。这个过程可以无限继续:)


由于我们按名称引用了以前的值,GHC很高兴地将它们保留在内存中,因此每个项只计算一次。

这样想。变量
fib
是指向延迟值的指针。(您可以将下面的延迟值视为数据结构,如(非真实语法)
lazy a=IORef(未计算(IO a))|计算a)
;即,它开始时未计算,然后计算时会“更改”为记住值的内容。)因为递归表达式使用变量
fib
,它们有一个指向相同延迟值的指针(它们“共享”数据结构)。当某人第一次计算fib时,它会运行thunk以获取值,并且该值会被记住。由于递归表达式指向相同的惰性数据结构,因此当他们计算它时,他们将看到已计算的值。当他们遍历惰性“无限列表”时,将只有一个“部分列表”在内存中,
zipWith
将有两个指向“列表”的指针,它们只是指向同一“列表”以前的成员的指针,因为它以指向同一列表的指针开始

请注意,这并不是真正的“记忆”;它只是引用同一变量的结果。通常不会“记忆”函数结果(以下内容将是低效的):


在标准斐波那契数列中,
2
,通常出现在
1
3
之间,发生了什么事?当然取决于执行情况,但一个合适的方法只会对列表中的每个元素进行一次评估。完美地利用了真空;在这里非常出色。非常棒的答案,感谢您为此投入时间。我将此外,一旦您不再需要结构的初始部分,垃圾收集器将注意到这一点,并自动释放内存以供重用。因此,即使以这种方式使用内存以避免重新计算,在任何时候,对于
fibs的多个元素的单个迭代,只有最后两项将保留在内存中e> .这个闭包的范围是如何精确确定的?我对slow\u fib n=slow\u fib(n-1)+slow\u fib(n-2)的情况感到困惑,为什么以前计算的值没有存储。这是函数范围和全局范围之间的区别吗?
*TraceFibs> fibs !! 5
Adding 0 and 1 to obtain 1
Adding 1 and 1 to obtain 2
Adding 1 and 2 to obtain 3
Adding 2 and 3 to obtain 5
5
*TraceFibs> fibs !! 5
5
*TraceFibs> fibs !! 6
Adding 3 and 5 to obtain 8
8
*TraceFibs> fibs !! 16
Adding 5 and 8 to obtain 13
Adding 8 and 13 to obtain 21
Adding 13 and 21 to obtain 34
Adding 21 and 34 to obtain 55
Adding 34 and 55 to obtain 89
Adding 55 and 89 to obtain 144
Adding 89 and 144 to obtain 233
Adding 144 and 233 to obtain 377
Adding 233 and 377 to obtain 610
Adding 377 and 610 to obtain 987
987
*TraceFibs>
> take 2 fibs
fibs () = 0 : 1 : zipWith tadd (fibs ()) (tail (fibs ()))