fold
不同之处似乎是一个常见的混淆来源,所以这里有一个更一般的概述:
[x1, x2, x3, x4 ... xn ]
考虑用一些函数f
和种子折叠一个 n 值列表z
。
foldl
是:
- 左联想:
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- 尾递归:遍历列表,然后产生值
- 懒惰:在需要结果之前不评估任何内容
- Backwards:
foldl (flip (:)) []
反转列表。
foldr
是:
- 右联想:
f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
- 递归到一个参数:每次迭代都适用
f
于下一个值和折叠列表其余部分的结果。
- 懒惰:在需要结果之前不评估任何内容
- Forwards:
foldr (:) []
返回未更改的列表。
这里有一个稍微微妙的点有时会让人绊倒:因为foldl
是向后的,每个应用程序都f
被添加到结果的外部;并且因为它是惰性的,所以在需要结果之前不会评估任何内容。这意味着要计算结果的任何部分,Haskell 首先遍历整个列表以构造嵌套函数应用程序的表达式,然后评估最外层的函数,并根据需要评估其参数。如果f
总是使用它的第一个参数,这意味着 Haskell 必须一直递归到最里面的项,然后向后计算f
.
这显然与大多数函数式程序员所熟知和喜爱的高效尾递归相去甚远!
事实上,即使在foldl
技术上是尾递归的,因为整个结果表达式是在评估任何内容之前构建的,foldl
可能会导致堆栈溢出!
另一方面,考虑foldr
。它也是惰性的,但是因为它是向前运行的,所以每个应用程序f
都会添加到结果内部。因此,为了计算结果,Haskell 构造了一个函数应用程序,其第二个参数是折叠列表的其余部分。如果f
在它的第二个参数中是惰性的——例如,一个数据构造函数——结果将是递增的惰性,只有在评估需要它的结果的某些部分时才会计算折叠的每一步。
所以我们可以看到为什么foldr
有时在无限列表foldl
上工作而不能:前者可以将无限列表延迟转换为另一个延迟无限数据结构,而后者必须检查整个列表以生成结果的任何部分。另一方面,foldr
对于一个同时需要两个参数的函数,例如(+)
, 工作(或者更确切地说,不工作)很像foldl
,在评估它之前构建一个巨大的表达式。
因此,需要注意的两个重点是:
foldr
可以将一种惰性递归数据结构转换为另一种。
- 否则,惰性折叠将在大型或无限列表上因堆栈溢出而崩溃。
您可能已经注意到,这听起来像是foldr
可以做任何事情foldl
,甚至更多。这是真的!事实上,foldl 几乎没用!
但是如果我们想通过折叠一个大的(但不是无限的)列表来产生一个非惰性结果呢?为此,我们需要标准库精心提供的严格折叠:
foldl'
是:
- 左联想:
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- 尾递归:遍历列表,然后产生值
- Strict:每个功能应用程序都在此过程中进行评估
- Backwards:
foldl' (flip (:)) []
反转列表。
因为foldl'
是strict,所以为了计算结果,Haskell 将在每一步求值 f
,而不是让左参数累积一个巨大的、未求值的表达式。这给了我们想要的通常的、有效的尾递归!换句话说:
foldl'
可以有效地折叠大列表。
foldl'
将挂在无限列表上的无限循环中(不会导致堆栈溢出)。
Haskell wiki 也有一个讨论这个的页面。