我不确定Scala中fold
和foldLeft
之间的区别。
问题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表示 乘法。)
为什么要添加任意次数?我认为折叠的工作方式不同。
答案 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
的值组合在一起 - 类型之间没有对应关系A
和B
。
示例。在您的示例中,假设第一个处理器采用元素"1", "2"
而第二个采用元素"3"
。第一个将生成折叠值3
,第二个将生成另一个折叠值3
。现在他们必须将他们的结果组合起来才能得到最终折叠值 - 这是不可能的,因为闭包_ + _.toInt
只知道如何组合Int
和String
,而不是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)
时,您认为0
,int
是String
的子类型。这就是编译器抛出错误的原因。
我们必须看到fold
的{{3}}来了解会发生什么。这是我们得到的:
def fold[A1 >: A](z: A1)(op: (A1, A1) => A1): A1 = foldLeft(z)(op)
基本上,fold
是foldleft
的实现,对输出类型有约束。
现在,我们可以看到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。