【问题标题】:How do you know when to use fold-left and when to use fold-right?你怎么知道什么时候使用 fold-left 和什么时候使用 fold-right?
【发布时间】:2010-11-29 14:32:25
【问题描述】:

我知道向左折叠会产生向左倾斜的树,向右折叠会产生向右倾斜的树,但是当我伸手去折叠时,我有时会发现自己陷入了令人头痛的想法,试图确定哪个种折叠是合适的。我通常最终会展开整个问题并逐步执行折叠功能,因为它适用于我的问题。

所以我想知道的是:

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

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

由于折叠是(函数式)编程中如此常见的操作,我希望能够快速而自信地做出这类决定。那么...有什么建议吗?

【问题讨论】:

  • 这个 Q 比那个更笼统,专门针对 Haskell。懒惰对问题的答案有很大影响。
  • 哦。奇怪,不知怎的,我以为我在这个问题上看到了 Haskell 标签,但我想不是......

标签: language-agnostic functional-programming fold


【解决方案1】:

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

这个例子使用累加器函数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 更普遍。但在其他情况下,中缀符号 + 括号非常有用。

【讨论】:

  • 嗯,你所描述的实际上是Haskell中的foldl1foldr1foldlfoldr取外部初始值),而Haskell的“缺点”称为(:)不是(::),否则这是正确的。您可能需要补充一点,Haskell 还提供了一个 foldl'/foldl1',它们是 foldl/foldl1 的严格变体,因为并不总是需要惰性算术。
  • 抱歉,我以为我在这个问题上看到了“Haskell”标签,但它不存在。如果不是 Haskell,我的评论就没有多大意义......
  • @ephemient 你确实看到了。它是“haskell 风格的伪代码”。 :)
  • 我见过的关于折叠之间差异的最佳答案。
【解决方案2】:

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

折叠: (((1 + 2) + 3) + 4)可以计算每个操作,并将累加值向前推进

文件夹: (1 + (2 + (3 + 4)))在计算3 + 4之前需要为1 + ?2 + ?打开一个栈帧,然后它需要回过头来分别计算。

我还不足以成为函数式语言或编译器优化方面的专家来说明这是否真的会有所作为,但使用带有交换运算符的 foldl 肯定看起来更干净。

【讨论】:

  • 额外的堆栈帧肯定会对大型列表产生影响。如果您的堆栈帧超过了处理器缓存的大小,那么您的缓存未命中将影响性能。除非列表是双向链接的,否则很难使 foldr 成为尾递归函数,因此除非有理由不这样做,否则应该使用 foldl。
  • Haskell 的懒惰性质混淆了这个分析。如果被折叠的函数在第二个参数中是非严格的,那么foldr 很可能比foldl 更高效,并且不需要任何额外的堆栈帧。
  • 抱歉,我以为我在这个问题上看到了“Haskell”标签,但它不存在。如果不是 Haskell,我的评论就没有多大意义......
【解决方案3】:

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

((1 + 2) + 3) + 4

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

1 + (2 + (3 + 4))

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

所以我提出了一条经验法则:如果它看起来像一个列表迭代,并且很容易以尾递归形式编写,那么 foldl 就是要走的路。

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

【讨论】:

    【解决方案4】:

    其他海报已经给出了很好的答案,我不会重复他们已经说过的话。正如您在问题中给出了一个 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 中是这样。否则,请参考答案中给出的其他建议;)。

    【讨论】:

    猜你喜欢
    • 2020-08-11
    • 2011-06-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-10
    • 2018-01-21
    • 2018-05-15
    • 2011-08-08
    相关资源
    最近更新 更多