fold
的区别似乎是常见的困惑源,下面是一个更一般性的概述:
考虑用某个函数f
和种子z
来折叠一个包含n个值的列表[x1, x2, x3, x4 ... xn ]
。
foldl
是:
- 左关联:
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- 尾递归: 它迭代整个列表,产生最终的值
- 惰性求值: 直到需要结果时才进行计算
- 反向:
foldl (flip (:)) []
将翻转一个列表。
foldr
是:
- 右关联:
f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
- 递归到参数中: 每次迭代将
f
应用于下一个值和剩余列表的折叠结果。
- 惰性求值: 直到需要结果时才进行计算
- 正向:
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
- 尾递归:它遍历列表并在之后产生值
- 严格:每个函数应用都会在过程中被求值
- 反向:
foldl'(flip(:))[]
将翻转列表。
因为 foldl'
是严格的,为了计算结果 Haskell 会在每一步 评估 f
,而不是让左参数积累一个庞大的未评估表达式。这给我们想要的通常、高效的尾递归!换句话说:
foldl'
可以高效地折叠大型列表。
foldl'
在无限列表上会陷入无限循环(而不会导致堆栈溢出)。
Haskell 维基也有关于此的页面。
foldr
比foldl
更好,而在Erlang中相反(我之前学过Haskell之前学的是Erlang)。由于Erlang不是惰性的,并且函数没有柯里化,所以在Erlang中的foldl
的行为就像上面的foldl'
。这是一个很好的答案!干得好,谢谢! - Siu Ching Pong -Asuka Kenji-foldl
描述为“向后”的函数,而将foldr
描述为“向前”的函数有些问题。部分原因是在说明fold为什么是“向后”时,将(:)
应用于flip
,这让人自然地想到,“当然是向后了:你flip
了列表拼接!” 另外,看到foldl
被称为“向后”,也有些奇怪,因为在完整的计算中,foldl
首先(最内层)将f
应用于第一个列表元素。实际上,是foldr
在“向后运行”,将f
应用于最后一个元素。 - Dave Abrahamsfoldl
和foldr
,并忽略严格性和优化的情况下,前者意味着“最外层”,而不是“最内层”。这就是为什么foldr
可以处理无限列表而foldl
不能 - 右折叠首先将f
应用于第一个列表元素和(未求值的)折叠尾部的结果,而左折叠必须遍历整个列表以评估f
的最外层应用。 - C. A. McCannlast xs = foldl (\a z-> z) undefined xs
。 - Will Nessf (foldl f [] [x1, x2,... xn-1]) xn
,并将foldr的左结合表示为f x1 (foldr f [] [x2, x3, ... xn])
更加清晰。在这种表示中,很明显foldr可以处理无限列表。 - Dagang