你怎么知道何时使用fold-left以及何时使用fold-right?

时间:2009-09-18 19:22:41

标签: language-agnostic functional-programming fold

我知道fold-left会产生左倾树木,而右倾产生右倾树木,但是当我伸手去寻找折叠时,我有时会发现自己陷入了令人头痛的想法,试图确定折叠是合适的。我通常最终会解决整个问题并逐步执行fold函数,因为它适用于我的问题。

所以我想知道的是:

  • 确定是向右折叠还是向右折叠有哪些经验法则?
  • 考虑到我面临的问题,如何快速决定使用哪种类型的折叠?

Scala by Example(PDF)中有一个使用折叠编写名为flatten的函数的示例,该函数将元素列表列表连接到单个列表中。在这种情况下,右侧折叠是正确的选择(考虑到列表连接的方式),但我必须考虑一下才能得出结论。

由于折叠是(功能)编程中的常见动作,我希望能够快速而自信地做出这些决定。所以...任何提示?

4 个答案:

答案 0 :(得分:98)

您可以将折叠转换为中缀运算符表示法(在两者之间写入):

此示例使用累加器函数x

进行折叠
fold x [A, B, C, D]

因此等于

A x B x C x D

现在你只需要推理你的操作员的关联性(通过括号!)。

如果你有一个左关联运算符,你可以像这样设置括号

((A x B) x C) x D

在这里,您使用左侧折叠。示例(haskell样式伪代码)

foldl (-) [1, 2, 3] == (1 - 2) - 3 == 1 - 2 - 3 // - is left-associative

如果您的运算符是右关联的(右侧折叠),则括号将设置如下:

A x (B x (C x D))

示例:Cons-Operator

foldr (:) [] [1, 2, 3] == 1 : (2 : (3 : [])) == 1 : 2 : 3 : [] == [1, 2, 3]

通常,算术运算符(大多数运算符)是左关联的,因此foldl更为普遍。但在其他情况下,中缀符号+括号非常有用。

答案 1 :(得分:59)

Olin Shivers通过说“foldl是基本列表迭代器”和“foldr是基本列表递归运算符”来区分它们。如果你看看foldl是如何工作的:

((1 + 2) + 3) + 4

你可以看到正在构建的累加器(如在尾递归迭代中)。相反,foldr继续:

1 + (2 + (3 + 4))

您可以在其中查看到基础案例4的遍历并从那里构建结果。

所以我提出了一个经验法则:如果它看起来像一个列表迭代,一个用尾递归形式写的很简单,那么foldl就是你的选择。

但实际上,这可能是您正在使用的运算符的关联性最明显的。如果它们是左关联的,请使用foldl。如果它们是右关联的,请使用foldr。

答案 2 :(得分:27)

其他海报给出了很好的答案,我不会重复他们已经说过的话。正如您在问题中给出了Scala示例,我将给出一个Scala特定示例。正如Tricks已经说过的那样,foldRight需要保留n-1堆栈帧,其中n是列表的长度,这很容易导致堆栈溢出 - 而不是即使是尾递归也可以避免这种情况。

List(1,2,3).foldRight(0)(_ + _)会缩减为:

1 + List(2,3).foldRight(0)(_ + _)        // first stack frame
    2 + List(3).foldRight(0)(_ + _)      // second stack frame
        3 + 0                            // third stack frame 
// (I don't remember if the JVM allocates space 
// on the stack for the third frame as well)

List(1,2,3).foldLeft(0)(_ + _)会减少为:

(((0 + 1) + 2) + 3)

可以迭代计算,如implementation of List中所做的那样。

在Scala严格评估的语言中,foldRight可以很容易地为大型列表填充堆栈,而foldLeft则不会。

示例:

scala> List.range(1, 10000).foldLeft(0)(_ + _)
res1: Int = 49995000

scala> List.range(1, 10000).foldRight(0)(_ + _)
java.lang.StackOverflowError
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRig...

因此,我的经验法则是 - 对于没有特定关联性的运算符,请始终使用foldLeft,至少在Scala中使用{{1}}。否则,请与答案中给出的其他建议一起使用;)。

答案 3 :(得分:4)

这也值得注意(我意识到这说明了一点),在交换运算符的情况下,两者几乎是等价的。在这种情况下,foldl可能是更好的选择:

与foldl: (((1 + 2) + 3) + 4)可以计算每个操作并将累计值转发

foldr相似: 在计算(1 + (2 + (3 + 4)))之前,1 + ?需要为2 + ?3 + 4打开堆栈框架,然后需要返回并为每个框架进行计算。

我不是一个关于函数式语言或编译器优化的专家来说明这是否真的会产生影响但是使用带有可交换运算符的foldl看起来更加清晰。