函数编程中的reduce和foldLeft / fold之间的区别(尤其是Scala和Scala API)?


Answers:


260

减少vs折叠

与该主题相关的任何其他stackoverflow答案中均未明确提及的一个很大的不同之处在于,reduce应该给其赋予可交换的monoid,即既可交换又可关联的操作。这意味着操作可以并行化。

这种区别对于大数据/ MPP /分布式计算非常重要,这reduce甚至说明了其存在的全部原因。可以将集合切碎,并且reduce可以对每个块进行操作,然后可以对每个块reduce的结果进行操作-实际上,分块的级别不必深陷一个级别。我们也可以切碎每个块。这就是为什么在给定无限数量的CPU的情况下将列表中的整数相加为O(log N)的原因。

如果你只是看签名,没有理由reduce存在,因为你可以做到的一切,你可以reducefoldLeft。的功能foldLeft大于的功能reduce

但是您不能并行化foldLeft,因此它的运行时间始终为O(N)(即使您输入可交换的monoid)。这是因为它假定该操作不是可交换的Monoid,因此将通过一系列顺序聚合来计算累积值。

foldLeft不假定可交换性或关联性。它具有关联性,可以对集合进行分解,而可交换性则使得累加变得容易,因为顺序并不重要(因此,从哪个顺序汇总每个块中的每个结果都没有关系)。严格来说,可交换性对于并行化不是必需的,例如分布式排序算法,它只是使逻辑变得更容易,因为您不需要给块排序。

如果查看一下Spark文档,reduce它会专门说“ ...交换和关联二进制运算符”

http://spark.apache.org/docs/1.0.0/api/scala/index.html#org.apache.spark.rdd.RDD

这证明reduce不只是以下情况的特例foldLeft

scala> val intParList: ParSeq[Int] = (1 to 100000).map(_ => scala.util.Random.nextInt()).par

scala> timeMany(1000, intParList.reduce(_ + _))
Took 462.395867 milli seconds

scala> timeMany(1000, intParList.foldLeft(0)(_ + _))
Took 2589.363031 milli seconds

减少对折

现在,这里离FP /数学根更近了,并且解释起来有些棘手。Reduce正式定义为MapReduce范式的一部分,该范式处理无序集合(多集),而Fold则以递归形式正式定义(请参见目录化),因此假定集合的结构/序列。

fold在Scalding中没有方法,因为在(严格的)Map Reduce编程模型下我们无法定义,fold因为块没有排序,fold只需要关联性,而不需要可交换性。

简而言之,reduce没有累积顺序的作品,fold需要累积的顺序,正是累积的顺序需要零值,而不是存在区分它们的零值。严格来说,reduce 应该对一个空集合起作用,因为它的零值可以通过取任意值x然后求解来推导x op y = x,但这不适用于非交换运算,因为可以存在一个左,右零值,它们是不同的(即x op y != y op x)。当然,Scala不会费心找出这个零值是什么,因为这需要做一些数学运算(这可能是不可计算的),因此只抛出一个异常。

似乎(在词源学中经常是这种情况)这种原始的数学意义已经丢失,因为编程中唯一明显的区别就是签名。结果是,它reduce已成为的同义词fold,而不是保留MapReduce的原始含义。现在,这些术语通常可以互换使用,并且在大多数实现中的行为相同(忽略空集合)。我们现在将要解决的特性(例如Spark中的特性)加剧了怪异。

因此,Spark 确实有一个fold,但是组合子结果(每个分区一个)(在编写本文时)的顺序与完成任务的顺序相同-因此不确定。由于@CafeFeed为指出fold的用途runJob,其中通过阅读代码后,我意识到,它的不确定性。Spark产生一个进一步的混乱,treeReduce但没有treeFold

结论

之间存在差异reducefold甚至应用于非空序列时。前者被定义为MapReduce编程范式的一部分,该范式是关于具有任意顺序的集合(http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf),其中一个应该假定运算符除了是可交换的以外关联给出确定性结果。后者是根据同构定义的,并且要求集合具有序列的概念(或像链表一样递归定义),因此不需要可交换的运算符。

在实践中,由于编程的unmathematical性质,reducefold趋向于在相同的行为方式,无论是正确(如在Scala中)或不正确(如在火花)。

额外:我对Spark API的看法

我的观点是,如果fold在Spark中完全删除该术语的使用,将避免混淆。至少spark在其文档中确实有注释:

这与以Scala之类的功能语言为非分布式集合实现的折叠操作有些不同。


2
这就是为什么在名称中foldLeft包含,Left以及为什么还有一个称为的方法的原因fold
kiritsuku 2014年

1
@Cloudtech这是它的单线程实现的巧合,不在其规范之内。在我的4核计算机上,如果尝试添加.par,则(List(1000000.0) ::: List.tabulate(100)(_ + 0.001)).par.reduce(_ / _)每次都会得到不同的结果。
samthebest 2014年

2
@AlexDean在计算机科学的上下文中,不,它实际上并不需要身份,因为空集合往往会引发异常。但是,如果在集合为空时返回identity元素,则从数学上讲更优雅(如果集合执行此操作会更优雅)。在数学中,“抛出异常”不存在。
samthebest 2014年

3
@samthebest:您确定可交换性吗?github.com/apache/spark/blob/…说:“对于非可交换的函数,结果可能与应用于非分布式集合的折叠结果不同。”
Make42 '16

1
@ Make42没错,reallyFold尽管可以这样写自己的皮条客,因为:rdd.mapPartitions(it => Iterator(it.fold(zero)(f)))).collect().fold(zero)(f),这不需要f上下班。
samthebest,2013年

10

如果我没记错的话,即使Spark API不需要它,折叠也需要f是可交换的。因为不能保证分区聚合的顺序。例如,在以下代码中,仅对第一个打印输出进行排序:

import org.apache.spark.{SparkConf, SparkContext}

object FoldExample extends App{

  val conf = new SparkConf()
    .setMaster("local[*]")
    .setAppName("Simple Application")
  implicit val sc = new SparkContext(conf)

  val range = ('a' to 'z').map(_.toString)
  val rdd = sc.parallelize(range)

  println(range.reduce(_ + _))
  println(rdd.reduce(_ + _))
  println(rdd.fold("")(_ + _))
}  

打印:

abcdefghijklmnopqrstuvwxyz

abcghituvjklmwxyzqrsdefnop

defghinopjklmqrstuvabcwxyz


经过一番往复,我们相信您是正确的。合并的顺序是先到先得。如果您sc.makeRDD(0 to 9, 2).mapPartitions(it => { java.lang.Thread.sleep(new java.util.Random().nextInt(1000)); it } ).map(_.toString).fold("")(_ + _)多次使用2个以上的内核运行,我想您会看到它会产生随机的(按分区划分)顺序。我已经相应更新了我的答案。
samthebest,2016年

3

foldApache Spark中的内容与fold未分发的集合中的内容不同。实际上,它需要交换函数才能产生确定性的结果:

这与以Scala之类的功能语言为非分布式集合实现的折叠操作有些不同。该折叠操作可以单独应用于分区,然后将那些结果折叠为最终结果,而不是以某些定义的顺序将折叠应用于每个元素。对于非交换函数,结果可能与应用于非分布式集合的折叠结果不同。

Mishael Rosenthal 已证明了这一点Make42其评论中建议了一点

有人建议观察到的行为与HashPartitioner何时parallelize不洗牌和不使用有关HashPartitioner

import org.apache.spark.sql.SparkSession

/* Note: standalone (non-local) mode */
val master = "spark://...:7077"  

val spark = SparkSession.builder.master(master).getOrCreate()

/* Note: deterministic order */
val rdd = sc.parallelize(Seq("a", "b", "c", "d"), 4).sortBy(identity[String])
require(rdd.collect.sliding(2).forall { case Array(x, y) => x < y })

/* Note: all posible permutations */
require(Seq.fill(1000)(rdd.fold("")(_ + _)).toSet.size == 24)

解释:

foldRDD的结构

def fold(zeroValue: T)(op: (T, T) => T): T = withScope {
  var jobResult: T
  val cleanOp: (T, T) => T
  val foldPartition = Iterator[T] => T
  val mergeResult: (Int, T) => Unit
  sc.runJob(this, foldPartition, mergeResult)
  jobResult
}

与RDD的结构reduce相同:

def reduce(f: (T, T) => T): T = withScope {
  val cleanF: (T, T) => T
  val reducePartition: Iterator[T] => Option[T]
  var jobResult: Option[T]
  val mergeResult =  (Int, Option[T]) => Unit
  sc.runJob(this, reducePartition, mergeResult)
  jobResult.getOrElse(throw new UnsupportedOperationException("empty collection"))
}

runJob不考虑分区顺序的情况下执行,导致需要交换功能。

foldPartition并且reducePartition在处理顺序上有效,reduceLeft并且foldLeft在上有效执行(通过继承和委派)TraversableOnce

结论:fold在RDD上不能依赖于块的顺序,而是需要可交换性和关联性


我必须承认,词源令人困惑,并且正式定义中缺乏编程文献。我认为可以肯定地说s确实foldRDDs相同reduce,但这并不尊重根本的数学差异(我将答案更新为更加清楚)。尽管我不同意我们的确需要交换性,只要他们有信心他们的分手正在做的事情,这就是维护秩序。
samthebest,2013年

未定义的折叠顺序与分区无关。这是runJob实现的直接结果。

啊!抱歉,我无法弄清楚您的意思是什么,但是通读runJob代码后,我发现确实可以根据任务完成的时间(而不是分区的顺序)进行合并。正是这一关键细节使一切都变得合理。我再次编辑了答案,从而更正了您指出的错误。由于我们现在已经达成协议,请取消您的悬赏吧?
samthebest,2013年

我无法编辑或删除-没有此类选项。我可以颁奖,但我想您单单从关注中可以得到很多意见,我错了吗?如果您确认要我奖励,我会在接下来的24小时内完成。感谢您的更正和方法的遗憾,但是您似乎忽略了所有警告,这是一件大事,并且在各处都引用了答案。

1
您如何将它授予@Mishael Rosenthal,因为他是第一个明确表示关注的人。我对这些要点没有兴趣,我只喜欢对SOO和组织使用SO。
samthebest,2016年

2

扩展的另一个不同之处是在Hadoop中使用组合器。

想象一下,您的操作是可交换的monoid,通过reduce将其应用于地图端,而不是将所有数据改组/排序到reducer。使用foldLeft并非如此。

pipe.groupBy('product) {
   _.reduce('price -> 'total){ (sum: Double, price: Double) => sum + price }
   // reduce is .mapReduceMap in disguise
}

pipe.groupBy('product) {
   _.foldLeft('price -> 'total)(0.0){ (sum: Double, price: Double) => sum + price }
}

在Scalding中将操作定义为monoid始终是一个好习惯。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.