欢迎来到懒惰评估的世界。
当您从严格评估的角度考虑它时, foldl 看起来“好”而 foldr 看起来“坏”,因为 foldl 是尾递归的,但是 foldr 必须在堆栈中构建一个塔,以便它可以首先处理最后一项。
然而,懒惰的评估扭转了局面。以map函数的定义为例:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
如果 Haskell 使用严格的评估,这不会太好,因为它必须先计算尾部,然后添加项目(对于列表中的所有项目)。似乎唯一有效的方法是反向构建元素。
但是,由于 Haskell 的惰性求值,这个 map 函数实际上是高效的。Haskell 中的列表可以被认为是生成器,这个 map 函数通过将 f 应用于输入列表的第一项来生成它的第一项。当它需要第二个项目时,它只是再次做同样的事情(不使用额外的空间)。
事实证明,map
可以用以下方式描述foldr
:
map f xs = foldr (\x ys -> f x : ys) [] xs
很难通过观察来判断,但是惰性求值会起作用,因为 foldr 可以f
立即给出它的第一个参数:
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
因为f
定义的 bymap
可以仅使用第一个参数返回结果列表的第一项,折叠可以在恒定空间中惰性操作。
现在,惰性评估确实会反击。例如,尝试运行 sum [1..1000000]。它会产生堆栈溢出。为什么要呢?它应该只是从左到右评估,对吧?
让我们看看 Haskell 是如何评估它的:
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
sum = foldl (+) 0
sum [1..1000000] = foldl (+) 0 [1..1000000]
= foldl (+) ((+) 0 1) [2..1000000]
= foldl (+) ((+) ((+) 0 1) 2) [3..1000000]
= foldl (+) ((+) ((+) ((+) 0 1) 2) 3) [4..1000000]
...
= (+) ((+) ((+) (...) 999999) 1000000)
Haskell 懒得执行添加。取而代之的是,它以一堆未经评估的笨拙而告终,不得不被迫获得一个数字。在此评估期间会发生堆栈溢出,因为它必须深度递归才能评估所有 thunk。
幸运的是,Data.List 中有一个特殊的函数被称为foldl'
严格操作。 foldl' (+) 0 [1..1000000]
不会堆栈溢出。(注意:我尝试在您的测试中替换foldl
为foldl'
,但实际上它使其运行速度变慢。)