我很好奇无限列表的运行时性能 下面的那个:
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
这将创建一个无限的斐波纳契序列表。
我的问题是,如果我执行以下操作:
takeWhile (<5) fibs
fibs
评估列表中的每个字词的次数是多少?它似乎
因为takeWhile
检查了每个项目的谓词函数
在列表中,fibs
列表将多次评估每个术语。该
前两个学期是免费的。当takeWhile
想要评估时
第3个元素(<5)
,我们将得到:
1 : 1 : zipWith (+) [(1, 1), (1)] => 1 : 1 : 3
现在,takeWhile
想要在第4个元素上评估(<5)
:
fibs
的递归性质将再次构造列表
以下内容:
1 : 1 : zipWith (+) [(1, 2), (2, 3)] => 1 : 1 : 3 : 5
我们似乎需要再次计算第3个元素
想要评估第4个元素的值。而且,如果
takeWhile
中的谓词很大,它表示函数是
做更多的工作,因为它正在评估前面的每一项
列表中的元素多次。我的分析在这里是正确的还是
Haskell做了一些缓存以防止多次评估?
答案 0 :(得分:80)
这是一个自引用的,懒惰的数据结构,其中结构的“后面”部分按名称引用较早的部分。
最初,该结构只是一个计算,未评估的指针返回自身。展开时,会在结构中创建值。稍后对已经计算过的结构部分的引用能够找到已经存在的值等待它们。无需重新评估这些部分,也无需额外的工作!
内存中的结构只是一个未评估的指针。一旦我们查看第一个值,它看起来像这样:
> take 2 fibs
(一个指向cons单元格的指针,指向'1',一个指向第二个'1'的尾部,以及一个指向一个函数的指针,该函数将引用保存回到fibs,以及fibs的尾部。
再评估一个步骤会扩展结构,并沿着以下步骤滑动参考:
所以我们开始展开结构,每次都产生一个新的未评估的尾部,这是一个封闭,将引用返回到最后一步的第一和第二个元素。这个过程可以无限延续:)
因为我们通过名称引用先前的值,所以GHC很乐意将它们保留在我们的记忆中,因此每个项目仅被评估一次。
答案 1 :(得分:31)
插图:
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>
答案 2 :(得分:19)
当在Haskell中评估某些内容时,只要它被相同名称 1 引用,它就会保持评估状态。
在以下代码中,列表l
仅评估一次(可能很明显):
let l = [1..10]
print l
print l -- None of the elements of the list are recomputed
即使部分评估了某些内容,该部分也会继续评估:
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
列表。这意味着没有元素被评估两次。
1 这当然不是语言语义严格要求的,但在实践中总是如此。
答案 3 :(得分:4)
这样想。变量fib
是指向惰性值的指针。 (您可以将下面的惰性值视为一种数据结构,如(非真实语法)Lazy a = IORef (Unevaluated (IO a) | Evaluated a)
;即它以thunk开始时未评估;然后在评估时,它“更改”为记住值的内容。)因为递归表达式使用变量fib
,所以它们具有指向相同惰性值的指针(它们“共享”数据结构)。有人第一次评估fib
时,它运行thunk以获取值并记住该值。并且因为递归表达式指向相同的惰性数据结构,所以当它们评估它时,它们将会看到已评估的值。当他们遍历懒惰的“无限列表”时,内存中只会有一个“部分列表”; zipWith
将有两个指向“列表”的指针,这些指针只是指向同一“列表”的先前成员的指针,因为它始于指向同一列表的指针。
请注意,这不是真正的“记忆”;这只是引用相同变量的结果。通常没有“记忆”功能结果(以下效率低下):
fibs () = 0 : 1 : zipWith tadd (fibs ()) (tail (fibs ()))