2009-09-18 104 views
82

我知道折叠左边会产生左倾的树木,而右折可以产生右倾的树木,但是当我达到折叠的时候,我有时会发现自己陷入了头痛的诱因之中,试图确定是哪一种的倍数是合适的。我通常最终解开整个问题,并逐步执行折叠函数,因为它适用于我的问题。你怎么知道何时使用折叠和何时使用折叠权?

所以,我想知道的是:

  • 是什么的一些经验法则来确定是否留下来折或折叠吧?
  • 鉴于我面临的问题,我该如何快速决定使用哪种折叠类型?

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

由于在功能性编程中折叠是一种常见的行为,我希望能够迅速而自信地做出这些决定。那么......任何提示?

+1

看起来类似于http:// stackoverflow。com/questions/384797/foldr-vs-foldl-or-foldl的含义 – 2009-09-18 20:57:25

+1

这个Q比这更具一般性,特别是关于Haskell。懒惰对问题的答案有很大的影响。 – 2009-09-19 00:44:29

+0

哦。奇怪的是,不知何故,我认为我在这个问题上看到了一个Haskell标签,但我猜不... – ephemient 2009-09-19 04:09:59

回答

84

您可以将折叠成一个管道符符号(写入之间):

这个例子倍使用累加器功能x

fold x [A, B, C, D] 

从而等于

A x B x C x D 

现在你只需要推断你的操作符的相关性(通过放置圆括号!)。

如果你有一个左结合运营商,您将设置括号这样

((A x B) x C) x D 

在这里,你使用左折叠。示例(哈斯克尔式的伪代码)

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

如果你的操作是右结合(右折),括号将设置这样的:

A x (B x (C x D)) 

例如:缺点运营商

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

一般来说,算术运算符(大多数运算符)是左关联的,所以foldl更广泛。但在其他情况下,中缀记法+圆括号非常有用。

+4

那么,你所描述的实际上是Haskell中的foldl1和foldr1(foldl和foldr需要一个外部初始值),Haskell的“cons”被称为'(:)'而不是'(::)',否则这是正确的。您可能想要补充的是,Haskell另外提供了'foldl'/'foldl1',它们是'foldl' /'foldl1'的严格变体,因为懒惰算术并不总是可取的。 – ephemient 2009-09-18 20:42:26

+0

对不起,我想我在这个问题上看到了一个“Haskell”标签,但它不在那里。我的评论真的没有多大意义,如果它不是Haskell ... – ephemient 2009-09-19 04:10:39

+0

@ephemient你确实看到它。这是“haskell风格的伪代码”。 :) – 2015-04-21 02:38:17

3

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

与foldl: (((1 + 2) + 3) + 4)可以计算出每一个操作和执行的累加值向前

foldr相似: (1 + (2 + (3 + 4)))需要一个堆栈帧计算之前打开的1 + ?2 + ?3 + 4,那么它需要返回并为每个计算。

我不是足够的功能语言或编译器优化的专家,说这是否会实际上有所作为,但它确实似乎更清洁使用foldl与交换操作符。

+0

额外的堆栈帧肯定会对大型列表产生影响。如果您的堆栈帧超过了处理器缓存的大小,那么您的缓存未命中将会影响性能。除非列表是双向链接的,否则使用foldr作为尾递归函数是很难的,所以除非有理由不使用foldl,否则应该使用foldl。 – 2009-09-18 20:44:33

+2

哈斯克尔的懒惰本质混淆了这种分析。如果被折叠的函数在第二个参数中是非严格的,那么'foldr'可以比'foldl'更有效,并且不需要任何额外的栈帧。 – ephemient 2009-09-18 20:52:10

+1

对不起,我想我在这个问题上看到了一个“Haskell”标签,但它不在那里。我的评论真的没有什么意义,如果它不是Haskell ... – ephemient 2009-09-19 04:11:14

56

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

((1 + 2) + 3) + 4 

你可以看到蓄电池(如在尾递归迭代)在建。相比之下,foldr相似收益:

1 + (2 + (3 + 4)) 

在这里你可以看到遍历的基本情况4,建立了从那里的结果。

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

但实际上,这可能是最明显的从您正在使用的操作符的关联性。如果他们是左联合的,请使用foldl。如果它们是正确关联的,请使用foldr。

22

其他海报已经给出了很好的答案,我不会重复他们已经说过的话。正如你在你的问题中给出了一个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完成。

在一个严格评估的语言斯卡拉,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,至少在斯卡拉运营商。否则,请与答案中给出的其他建议;)。

+6

这是真的,但在当前版本的Scala中,foldRight已被更改为在列表的反转副本上应用foldLeft。例如,在2.10.3中,https://github.com/scala/scala/blob/v2.10.3/src/library/scala/collection/immutable/List.scala#L305。看起来像2013年初发生的这种变化 - https://github.com/scala/scala/commit/6db4db93a7d976f1a3b99f8f1bffff23a1ae3924。 – 2014-09-05 06:59:17