归结为惰性评估。让我们使用 augustss 的定义,因为它稍微简单一些,但将其称为 big 而不是 xs,因为该标识符通常用于实用程序中。
Haskell 仅在必要时评估代码。如果不需要某些东西,那里有一个存根,基本上是一个指向函数闭包的指针,可以在需要时计算值。
假设我想评估big !! 4。 !! 的定义是这样的:
[] !! _ = error "Prelude.(!!): index too large"
(x:_) !! 0 = x
(_:xs) !! n = xs !! (n-1)
big的定义是
big = 1 : [sum (map (^2) ys) | ys <- tail (inits big)]
因此,在评估索引访问时,首先必须选择正确的函数变体。列表数据类型有两个构造函数,[] 和 first : rest。调用是big !! 4,!!的第一个分支只是检查列表是否是[]。由于列表显式以1 : stub1 开头,因此答案是否定的,分支被跳过。
第二个分支想知道是否选择了first : rest 形式。答案是肯定的,first 是 1 和 rest 是那么大的理解 (stub1),它的价值无关紧要。但是第二个参数不是0,所以这个分支也被跳过了。
第三个分支也匹配first : last,但是第二个参数接受任何东西,所以它适用。它忽略first,将xs 绑定到未评估的理解stub1,并将n 绑定到4。然后它递归地调用自己,第一个参数是理解,第二个参数是 3。 (从技术上讲,这是(4-1),尚未评估,但作为简化,我们假设它是。)
递归调用必须再次评估其分支。第一个分支检查第一个参数是否为空。由于到目前为止的论点是一个未评估的存根,因此需要对其进行评估。但仅足以决定分支是否为空。所以让我们开始评估理解:
stub1 = [sum (map (^2) ys) | ys <- tail (inits big)]
我们首先需要的是ys。它设置为tail (inits big)。 tail 很简单:
tail [] = []
tail (_:xs) = xs
inits 实现起来相当复杂,但重要的是它会延迟生成结果列表,即如果你给它(x:unevaluated),它会在评估其余部分之前生成[] 和[x]列表。换句话说,如果您不超越这些,它将永远不会评估其余部分。
所以,到目前为止,big 已知为(1 : stub1),所以inits 返回[] : stub2。 tail 与此匹配,选择它的第二个分支,并返回 stub2。 stub2 是big 在无所不在的空列表之后的inits 列表,尚未生成。
列表推导然后尝试将ys 的第一个元素的值提供给stub2,因此必须对其进行评估。 inits 的第二个结果仍然是已知的,它是 [1]。 ys 获取该值。此时,big 已知为1 : stub3 : stub4,其中stub3 = sum (map (^2) [1]) 和stub4 是第一次迭代后的列表推导。
由于big 现在被进一步评估,stub1 也是如此。现在知道它是stub3 : stub4,我们终于可以在!! 前进了。第一个分支不适用,因为列表不为空。第二个分支不适用,因为3 /= 0。第三个分支适用,将xs 绑定到stub4 和n 到3。递归调用是stub4 !! 2。
我们需要评估一下stub4。这意味着我们进入了理解的下一次迭代。我们需要inits big 的第三个元素。由于big 现在已知为1 : stub3 : stub4,因此无需进一步评估即可将第三个元素计算为[1, stub3]。 ys 绑定到该值,stub4 计算结果为 stub5 : stub6,其中 stub5 = sum (map (^2) [1, stub3]) 和 stub6 是前两次迭代后的理解。评估了stub4,我们现在知道big = 1 : stub3 : stub5 : stub6。
所以stub4 仍然不匹配!! 的第一个分支(永远不会匹配,因为我们正在处理一个无限列表)。 2 仍然与第二个分支不匹配。我们有另一个递归调用,然后是另一个,遵循与到目前为止相同的模式。当索引最终达到 0 时,我们有:
big = 1 : stub3 : stub5 : stub7 : stub9 : stub10
stub3 = sum (map (^2) [1])
stub5 = sum (map (^2) [1, stub3])
stub7 = sum (map (^2) [1, stub3, stub5])
stub9 = sum (map (^2) [1, stub3, stub5, stub7])
stub10 = whatever remains of the list comprehension
我们当前的调用是(stub9 : stub10) !! 0,最终匹配到第二个分支。 x 绑定到 stub9 并返回。
只有现在,如果您真的尝试打印或以其他方式处理 x,所有这些存根最终都会被评估为实际数字。