foldr 与 foldl(或 foldl')的含义

Posted

技术标签:

【中文标题】foldr 与 foldl(或 foldl\')的含义【英文标题】:Implications of foldr vs. foldl (or foldl')foldr 与 foldl(或 foldl')的含义 【发布时间】:2010-09-27 22:34:33 【问题描述】:

首先,我正在阅读的Real World Haskell说永远不要使用foldl,而是使用foldl'。所以我相信它。

但我不清楚何时使用 foldrfoldl'。虽然我可以在我面前看到它们如何以不同方式工作的结构,但我太愚蠢了,无法理解何时“哪个更好”。我想在我看来,使用哪个并不重要,因为它们都产生相同的答案(不是吗?)。事实上,我之前对这种构造的经验来自 Ruby 的 inject 和 Clojure 的 reduce,它们似乎没有“左”和“右”版本。 (附带问题:他们使用哪个版本?)

任何可以帮助像我这样聪明的人的见解将不胜感激!

【问题讨论】:

【参考方案1】:

foldr f x ys 的递归,其中ys = [y1,y2,...,yk] 看起来像

f y1 (f y2 (... (f yk x) ...))

foldl f x ys 的递归看起来像

f (... (f (f x y1) y2) ...) yk

这里的一个重要区别是,如果f x y 的结果可以仅使用x 的值来计算,那么foldr 不需要检查整个列表。例如

foldr (&&) False (repeat False)

返回False

foldl (&&) False (repeat False)

永不终止。 (注意:repeat False 创建一个无限列表,其中每个元素都是False。)

另一方面,foldl' 是尾递归且严格的。如果您知道无论如何都必须遍历整个列表(例如,将列表中的数字相加),那么foldl'foldr 更节省空间(也可能是时间)。

【讨论】:

在foldr中它的计算结果是f y1 thunk,所以它返回False,但是在foldl中,f不能知道它的任何一个参数。在Haskell中,无论它是否是尾递归,它都会导致 thunk 溢出,即 thunk 太大。 foldl' 可以在执行过程中立即减少 thunk。 为避免混淆,请注意括号不显示实际的评估顺序。由于 Haskell 是惰性的,最外层的表达式将首先被计算。 很好的答案。我想补充一点,如果你想要一个可以在列表中途停止的折叠,你必须使用 foldr;除非我弄错了,否则无法停止左折叠。 (当你说“如果你知道......你会......遍历整个列表”时,你暗示了这一点)。此外,“仅使用值”的错字应更改为“仅使用值”。 IE。去掉“开”字。 (*** 不允许我提交 2 个字符的更改!)。 @Lqueryvg 停止左折叠的两种方法: 1. 用右折叠对其进行编码(参见fodlWhile); 2. 将其转换为左扫描 (scanl) 并使用last . takeWhile p 或类似名称停止。嗯,3. 使用mapAccumL。 :) @Desty 因为它会在每个步骤中生成其整体结果的新部分——与 foldl 不同,foldl 会收集其整体结果并仅在所有工作完成且没有更多步骤后才生成它去表演。所以例如foldl (flip (:)) [] [1..3] == [3,2,1],所以 scanl (flip(:)) [] [1..] = [[],[1],[2,1],[3,2,1],...]... IOW、foldl f z xs = last (scanl f z xs)无限列表没有最后一个元素(在上面的示例中,它本身就是一个无限列表,从 INF 到 1 )。【参考方案2】:

它们的语义不同,因此您不能只互换foldlfoldr。一个从左侧折叠元素,另一个从右侧折叠。这样,操作符就会以不同的顺序被应用。这对于所有非关联运算(例如减法)都很重要。

Haskell.org 在这个主题上有一个有趣的article。

【讨论】:

它们的语义只有细微的差别,在实践中没有意义:使用函数的参数顺序。所以在接口方面,它们仍然算作可交换的。真正的区别似乎只是优化/实现。 @Evi1M4chine 没有任何区别是微不足道的。相反,它们是实质性的(是的,在实践中是有意义的)。事实上,如果我今天写这个答案,它会更加强调这种差异。【参考方案3】:

简而言之,foldr 在累加器函数在其第二个参数上是惰性的时会更好。阅读 Haskell wiki 的 Stack Overflow(双关语)了解更多信息。

【讨论】:

【参考方案4】:

foldl' 在 99% 的所有用途中优于 foldl 的原因是它可以在大多数用途中在恒定空间中运行。

取函数sum = foldl['] (+) 0。当使用foldl' 时,会立即计算总和,因此将sum 应用于无限列表将永远运行,并且很可能在恒定空间中(如果您使用Ints 之类的东西,Doubles , Floats。如果数字大于maxBound :: IntIntegers 将使用超过常量空间。

使用foldl,建立了一个thunk(就像如何获得答案的配方,可以稍后评估,而不是存储答案)。这些 thunk 会占用大量空间,在这种情况下,评估表达式要比存储 thunk 好得多(导致堆栈溢出……并导致您……哦,没关系)

希望对您有所帮助。

【讨论】:

最大的例外是如果传递给foldl 的函数只是将构造函数应用于其一个或多个参数。 foldl实际上是最佳选择时,是否存在一般模式? (就像foldr 是错误选择时的无限列表一样,优化方面。?) @Evi1M4chine 不确定foldr 是无限列表的错误选择是什么意思。事实上,您不应该将foldlfoldl' 用于无限列表。见Haskell wiki on stack overflows【参考方案5】:

foldr 看起来像这样:

foldl 看起来像这样:

上下文:Haskell wiki 上的Fold

【讨论】:

我区分两者的首选方式是这样的:foldl 将括号堆叠在左侧,foldr 将括号堆叠在右侧:(((0+1)+2)+3)(1+(2+(3+0)))【参考方案6】:

正如Konrad 指出的那样,它们的语义不同。他们甚至没有相同的类型:

ghci> :t foldr
foldr :: (a -> b -> b) -> b -> [a] -> b
ghci> :t foldl
foldl :: (a -> b -> a) -> a -> [b] -> a
ghci> 

例如,列表追加运算符(++)可以用foldr as实现

(++) = flip (foldr (:))

同时

(++) = flip (foldl (:))

会给你一个类型错误。

【讨论】:

它们的类型是一样的,只是调换了一下,无关紧要。它们的界面和结果是相同的,除了讨厌的 P/NP 问题(阅读:无限列表)。 ;) 据我所知,由于实现的优化是实践中唯一的区别。 @Evi1M4chine 这是不正确的,看这个例子:foldl subtract 0 [1, 2, 3, 4] 计算为-10,而foldr subtract 0 [1, 2, 3, 4] 计算为-2foldl 实际上是 0 - 1 - 2 - 3 - 4foldr4 - 3 - 2 - 1 - 0 @krapht,foldr (-) 0 [1, 2, 3, 4]-2foldl (-) 0 [1, 2, 3, 4]-10。另一方面,subtract 与您的预期相反(subtract 10 144),所以 foldr subtract 0 [1, 2, 3, 4]-10foldl subtract 0 [1, 2, 3, 4]2(正数)。【参考方案7】:

顺便说一句,Ruby 的 inject 和 Clojure 的 reducefoldl(或 foldl1,取决于您使用的版本)。通常,当一种语言只有一种形式时,它是左折叠,包括 Python 的reduce、Perl 的List::Util::reduce、C++ 的accumulate、C# 的Aggregate、Smalltalk 的inject:into:phparray_reduce、Mathematica 的Fold 等 Common Lisp 的 reduce 默认为左折叠,但有一个右折叠选项。

【讨论】:

这条评论很有帮助,但我会感谢来源。 Common Lisp 的 reduce 并不懒惰,所以它是 foldl',这里的许多注意事项都不适用。 我想你的意思是foldl',因为它们是严格的语言,不是吗?否则,这不是意味着所有这些版本都会像 foldl 那样导致堆栈溢出吗?

以上是关于foldr 与 foldl(或 foldl')的含义的主要内容,如果未能解决你的问题,请参考以下文章

Haskell 的 foldr/l 和 Clojure 的 reduce

SML/NJ - 使用 foldr 的一种线长函数

为啥在 Racket 中以一种奇怪的方式定义 foldl?

Haskell - 严格与非严格与 foldl

使用`foldl`实现Haskell的`take`函数

Haskell:foldl'累加器参数