折叠和foldLeft方法的区别

时间:2012-07-03 21:00:36

标签: scala

我不确定Scala中foldfoldLeft之间的区别。

问题Difference between fold and foldLeft or foldRight?有一个关于排序的答案。这是可以理解的。但我仍然不明白为什么这样做(来自REPL):

scala> Array("1","2","3").foldLeft(0)(_ + _.toInt)
res6: Int = 6

但这不是:

scala> Array("1","2","3").fold(0)(_ + _.toInt)
<console>:8: error: value toInt is not a member of Any
              Array("1","2","3").fold(0)(_ + _.toInt)
                                               ^

此错误消息的含义是什么?

文档中的这一行也让我感到困惑。

  

z - 折叠操作的中性元素;可能会添加到   结果是任意次数,并且不得更改结果   (例如,Nil表示列表连接,0表示加法,或1表示   乘法。)

为什么要添加任意次数?我认为折叠的工作方式不同。

7 个答案:

答案 0 :(得分:73)

根据Scala的定义,foldLeft是线性操作,而fold是允许树操作。例如:

List(1,2,3,4,5).foldLeft(0)(_ + _)
// This is the only valid order of operations
0+1 = 1
      1+2 = 3
            3+3 = 6
                  6+4 = 10
                        10 + 5 = 15
                                 15  // done

List(1,2,3,4,5).fold(0)(_ + _)
// This is valid
0+1 = 1             0+3 = 3           0+5 = 5
      1+2 = 3             3+4 = 7           5
            3         +         7=10        5
                                  10    +   5 = 15
                                                15  // done

为了允许顺序列表的任意树分解,你必须有一个不做任何事情的零(所以你可以在树中的任何地方添加它),你必须创建同样的东西,你将二进制参数作为二进制参数,因此类型不会因你分解树而改变。

(能够作为树进行评估对于并行化很有用。如果您希望能够随时转换输出时间,则需要组合运算符标准起始值-transforms-sequence-element-to-desired-type function就像foldLeft一样.Scala有这个并称之为aggregate,但在某些方面,这更像foldLeft而不是{{} 1}}是。)

答案 1 :(得分:29)

我不熟悉Scala,但Scala的集合库与Haskell的设计类似。这个答案基于Haskell,对Scala也可能是准确的。

因为foldLeft从左到右处理其输入,所以它可以具有不同的输入和输出类型。另一方面,fold可以按各种顺序处理其输入,因此输入和输出必须具有相同的类型。通过扩展折叠表达式,这是最容易看到的。 foldLeft按特定顺序运作:

Array("1","2","3").foldLeft(0)(_ + _.toInt)
= ((0 + "1".toInt) + "2".toInt) + "3".toInt

请注意,数组元素从不用作组合函数的第一个参数。它们始终显示在+的右侧。

fold不保证特定订单。它可以做各种事情,例如:

Array("1","2","3").fold(0)(_ + _.toInt)
=  ((0 + "1".toInt) + "2".toInt) + "3".toInt
or (0 + "1".toInt) + ("2" + "3".toInt).toInt
or "1" + ("2" + ("3" + 0.toInt).toInt).toInt

数组元素可以出现在组合函数的任一参数中。但是你的组合函数希望它的第一个参数是一个int。如果您不遵守该约束,则最终会向int添加字符串!类型系统会捕获此错误。

可以多次引入中性元素,因为通常通过分割输入和执行多个连续折叠来实现平行折叠。连续折叠引入中性元素一次。想象一下Array(1,2,3,4).fold(0)(_ + _)的一个特定执行,其中数组被分成两个独立的数组,并且这些数组在两个线程中顺序折叠。 (当然,真正的fold函数不会将数组吐出到多个数组中。)一个线程执行Array(1,2).fold(0)(_ + _),计算0 + 1 + 2。另一个线程执行Array(3,4).fold(0)(_ + _),计算0 + 3 + 4。最后,将两个线程的部分和加在一起。请注意,中性元素0出现两次。

答案 2 :(得分:14)

注意:我在这里可能完全错了。我的scala不太完美。

我认为差异在于方法的签名:

def fold[A1 >: A](z: A1)(op: (A1, A1) ⇒ A1): A1

VS

def foldLeft[B](z: B)(op: (B, T) ⇒ B): B

简而言之,fold被定义为在某种类型A1上运行,它是数组类型的超类型,对于您的字符串数组,编译器将其定义为“Any”(可能是因为它需要一种可以存储String 或组合方法传递给fold Fold的int-notice采用了两个相同类型的参数?)这也是文档在讨论z时的意思 - Fold的实现可能是这样的,它结合了你的并行输入,例如:

"1" + "2" --\
             --> 3 + 3 -> 6
"3" + *z* --/

另一方面,foldLeft在B类(无约束)上运行,并且只要求您提供一个组合器方法,该方法接受类型B的参数和数组类型的另一个类型(在您的情况下为String),并生成一个B

答案 3 :(得分:14)

错误。您收到编译时错误,因为fold的签名只允许折叠类型的值,该类型是集合中值的类型的超类型,并且String(您的集合类型)和Int(您提供的零元素的类型)的唯一超类型是Any。因此,折叠结果的类型推断为Any - 而Any没有方法toInt

请注意,fold的两个版本具有不同的签名:

fold[A1 >: A](z: A1)(op: (A1, A1) => A1): A1

foldLeft[B](z: B)(f: (B, A) => B): B

为什么他们有不同的签名?这是因为fold可以并行实现,就像并行集合一样。当多个处理器折叠集合中的值时,每个处理器都会获取A类型的元素子集,并通过连续应用A1生成类型为op的折叠值。这些处理器产生的结果必须组合成一个最终的折叠值 - 这是使用op函数完成的,这正是这样做的。

现在,请注意,使用f中的foldLeft无法完成此操作,因为每个处理器都会生成类型为B的折叠值。使用B无法合并多个f类型的值,因为f仅将值B与另一个类型A的值组合在一起 - 类型之间没有对应关系AB

示例。在您的示例中,假设第一个处理器采用元素"1", "2"而第二个采用元素"3"。第一个将生成折叠值3,第二个将生成另一个折叠值3。现在他们必须将他们的结果组合起来才能得到最终折叠值 - 这是不可能的,因为闭包_ + _.toInt只知道如何组合IntString,而不是2 {{1值。

对于这些类型不同的情况,请使用Int,您必须在其中定义如何组合aggregate类型的两个值:

B

上面的def aggregate[B](z: B)(seqop: (B, A) => B, combop: (B, B) => B): B 定义了折叠结果和集合中的元素具有不同类型时如何执行最后一步。

中性元素。如上所述,多个处理器可以折叠集合中的元素子集。他们中的每一个都会通过添加中性元素来开始折叠值。

在以下示例中:

combop

始终返回List(1, 2, 3).foldLeft(4)(_ + _)

但是,10 = 4 + 1 + 2 + 3不应与4一起使用,因为它不是中性元素:

fold

上述内容可能会返回List(1, 2, 3).fold(4)(_ + _) (4 + 1 + 2) + (4 + 3) = 14。如果不对(4 + 1) + (4 + 2) + (4 + 3) = 18使用中性元素,则结果是不确定的。同样,您可以将fold用作中性元素,但不能使用非空列表。

答案 4 :(得分:5)

一般差异

以下是方法的原型

fold[A1 >: A](z: A1)(op: (A1, A1) ⇒ A1): A1
foldLeft[B](z: B)(f: (B, A) ⇒ B): B

因此,对于折叠,结果是A1 >: A类型而不是任何B。此外,正如文档中所指定的,fold订单不是

关于您的错误

在键入scala> Array("1","2","3").fold(0)(_ + _.toInt)时,您认为0intString的子类型。这就是编译器抛出错误的原因。

关于折叠中的怪异z

我们必须看到fold的{​​{3}}来了解会发生什么。这是我们得到的:

def fold[A1 >: A](z: A1)(op: (A1, A1) => A1): A1 = foldLeft(z)(op)

基本上,foldfoldleft的实现,对输出类型有约束。 现在,我们可以看到z实际上的使用方式与foldleft相同。因此,我们可以得出这样的评论,因为在未来的实现中没有任何保证这种行为。我们现在已经可以看到它implementation

def fold[U >: T](z: U)(op: (U, U) => U): U = {
  executeAndWaitResult(new Fold(z, op, splitter))
}

答案 5 :(得分:5)

正如另一个答案所指出的,fold方法主要用于支持并行折叠。你可以看到如下。首先,我们可以为整数定义一种包装器,它允许我们跟踪对其实例执行的操作。

case class TrackInt(v: Int) {
  val log = collection.mutable.Buffer.empty[Int]
  def plus(that: TrackInt) = {
    this.log += that.v
    that.log += this.v
    new TrackInt(this.v + that.v)
  }
}

接下来,我们可以创建这些东西的并行集合和一个标识元素:

val xs = (1 to 10).map(TrackInt(_)).par
val zero = TrackInt(0)

首先我们尝试foldLeft

scala> xs.foldLeft(zero)(_ plus _)
res0: TrackInt = TrackInt(55)

scala> zero.log
res1: scala.collection.mutable.Buffer[Int] = ArrayBuffer(1)

因此,我们的零值仅使用一次,正如我们所期望的那样,因为foldLeft执行顺序折叠。接下来,我们可以清除日志并尝试fold

scala> zero.log.clear()

scala> xs.fold(zero)(_ plus _)
res2: TrackInt = TrackInt(55)

scala> zero.log
res3: scala.collection.mutable.Buffer[Int] = ArrayBuffer(1, 6, 2, 7, 8)

因此我们可以看到折叠已经以这样的方式并行化,即多次使用零值。如果我们再次运行它,我们可能会在日志中看到不同的值。

答案 6 :(得分:0)

已经提到过,但没有示例:如果要允许具有不同数据类型的并行性用于输出和输入,则可以使用aggregate

Array("1","2","3").aggregate(0)(_ + _.toInt, _ + _)

第一个函数被首先调用。然后使用第二个函数减少其结果。参见Explanation of the aggregate scala function