folder vs.foldl(或foldl')的含义


155

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

但我在朦胧时使用foldrfoldl'。尽管我可以看到它们摆在我面前的方式各不相同,但我还是很愚蠢,无法理解何时“哪个更好”。我想在我看来,使用哪个并不重要,因为它们都产生相同的答案(不是吗?)。实际上,我以前使用此构造的经验来自Ruby inject和Clojure的reduce,它们似乎没有“ left”和“ right”版本。(旁问:他们使用哪个版本?)

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

Answers:


174

递归在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


2
在foldr中它的值为f y1 thunk,所以它返回False,但是在foldl中,f不能知道它的任何一个参数。在Haskell中,无论是否尾部递归,都可能导致thunk溢出,即thunk是太大。foldl'可以在执行过程中立即减少重击。
索耶

25
为避免混淆,请注意,括号中没有显示实际的评估顺序。由于Haskell是惰性的,因此将首先评估最外面的表达式。
2013年

很好的答案。我想补充一点,如果您想要一个可以在列表中部分停止的折叠,则必须使用foldr;除非我弄错了,否则不能停止左折。(当您说“如果您知道...您将...遍历整个列表”时,便会暗示这一点)。另外,“仅使用值”的错字应改为“仅使用值”。即删除单词“上”。(Stackoverflow不允许我提交2个字符的更改!)。
Lqueryvg 2014年

@Lqueryvg停止左折的两种方法:1.用右折对其进行编码(请参阅参考资料fodlWhile);2.将其转换为左扫描(scanl),然后使用last . takeWhile p或类似的选项停止扫描。嗯,并使用3 mapAccumL.。:)
Will Ness

1
@Desty,因为它在每个步骤中都会产生其整体结果的新部分-与不同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)。
Will Ness


33

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

Haskell.org上有一篇有趣的文章


它们的语义仅以简单的方式不同,在实践中没有意义:所使用函数的参数顺序。因此,从界面角度来看,它们仍然可以交换。真正的区别似乎只是优化/实现。
Evi1M4chine

2
@ Evi1M4chine这些差异都不是微不足道的。相反,它们是实质性的(是的,实际上是有意义的)。实际上,如果我今天写这个答案,它将更加强调这种差异。
康拉德·鲁道夫

23

不久,foldr当累加器函数在其第二个参数上延迟时会更好。在Haskell Wiki的堆栈溢出(双关语意味深远)上了解更多信息。


18

之所以对所有用途的99%foldl'首选,foldl是因为对于大多数用途,它可以在恒定的空间中运行。

采取功能sum = foldl['] (+) 0。当foldl'被使用时,总和立即计算,因此应用sum到无限的名单将只是在不断的空间一直运行下去,而且极有可能(如果你使用喜欢的东西IntS,Doubles和Float秒。Integer旨意使用超过如果常量空间数字变得大于maxBound :: Int)。

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

希望有帮助。


1
最大的例外是,如果传递给的函数foldl除了将构造函数应用于其一个或多个参数之外,什么都不做。
dfeuer

何时foldl才是最佳选择是否有通用的模式?(就像无限列表一样,什么时候foldr是错误的选择,优化方式??)
Evi1M4chine

@ Evi1M4chine不知道您是什么意思,foldr因为它是无限列表的错误选择。实际上,您不应使用foldl或将其foldl'用于无限列表。参见Haskell Wiki上的堆栈溢出信息
KevinOrr

14

顺便说一下,R​​uby inject和Clojure reducefoldl(或foldl1,取决于您使用的版本)。通常,当一种语言中只有一种形式时,它是左折的,包括Python的reduce,Perl的List::Util::reduce,C ++的accumulate,C#的Aggregate,Smalltalk的inject:into:,PHP的array_reduce,Mathematica的Fold等。CommonLisp的reduce默认值是左折,但是可以选择右折。


2
此评论很有帮助,但我希望您能获得消息来源。
titaniumdecoy

Common Lisp reduce并不懒惰,所以它是懒惰的,这里的foldl'许多注意事项都不适用。
MicroVirus

我想您是说foldl',因为它们是严格的语言,不是吗?否则,这是否意味着所有这些版本都会导致堆栈溢出foldl
Evi1M4chine

6

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

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

例如,列表附加运算符(++)可以用foldras 实现

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

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

将给您输入错误。


它们的类型相同,只是切换而已,这是无关紧要的。它们的界面和结果相同,除了令人讨厌的P / NP问题(阅读:无限列表)。;)据我所知,实现的优化是实践中的唯一区别。
Evi1M4chine

@ Evi1M4chine这是不正确的,请看以下示例:foldl subtract 0 [1, 2, 3, 4]计算为-10,而foldr subtract 0 [1, 2, 3, 4]计算为-2foldl实际上是,0 - 1 - 2 - 3 - 4foldris是4 - 3 - 2 - 1 - 0
krapht

@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(正)。
pianoJames
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.