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

时间:2014-08-06 11:07:15

标签: scala functional-programming reduce fold scalding

为什么Scala和像Spark和Scalding这样的框架同时具有reducefoldLeft?那么reducefold之间有什么区别?

4 个答案:

答案 0 :(得分:245)

reduce vs foldLeft

与此主题相关的任何其他stackoverflow答案中没有提到的一个很大的区别是,reduce应该被赋予可交换的monoid ,即一个既可交换又可交换的操作关联。这意味着操作可以并行化。

这种区别对于大数据/ MPP /分布式计算非常重要,以及reduce甚至存在的全部原因。可以对集合进行切割,reduce可以对每个块进行操作,然后reduce可以对每个块的结果进行操作 - 事实上,分块级别不需要停止一个级别。我们也可以砍掉每一块。这就是为什么如果给定无限数量的CPU,则对列表中的整数求和为O(log N)。

如果您只是查看签名,则reduce没有理由存在,因为您可以使用reduce foldLeft来实现所有目标。{1}} foldLeft的功能大于reduce的功能。

但是你无法并行化foldLeft,所以它的运行时总是O(N)(即使你输入一个可交换的monoid)。这是因为它假设操作是可交换的幺半群,因此累积值将通过一系列顺序聚合来计算。

foldLeft不承担交换性和相关性。它的相关性使得能够切割集合,并且它的交换性使累积变得容易,因为顺序并不重要(因此,汇总每个结果的顺序并不重要)每个块)。严格来说,交换不是并行化所必需的,例如分布式排序算法,它只是使逻辑更容易,因为你不需要给你的块排序。

如果您查看reduce的Spark文档,它会明确说明" ...可交换和关联二元运算符"

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范例的一部分,MapReduce范例处理无序集合(multisets),Fold在递归方面正式定义(参见catamorphism),因此假定集合的结构/序列。

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

简单地说,reduce在没有累积顺序的情况下工作,fold需要累积的顺序,并且累积的顺序需要零值而不是存在区分它们的零值。严格来说,reduce 应该处理空集合,因为它的零值可以通过取任意值x然后求解x op y = x推断出来,但是这并不是#39; t使用非交换操作,因为可能存在不同的左右零值(即x op y != y op x)。当然,Scala并不需要弄清楚这个零值是什么,因为需要做一些数学(这可能是不可计算的),所以只是抛出异常。

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

所以Spark 确实有一个fold,但是子结果(每个分区一个)组合的顺序(在编写本文时)与任务的顺序相同完成 - 因此是非确定性的。感谢@CafeFeed指出fold使用runJob,在阅读完代码后我意识到它是非确定性的。 Spark产生了进一步的混淆,treeReduce但没有treeFold

结论

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

在实践中由于编程的非数学性质,reducefold往往以相同的方式表现,正确(如在Scala中)或不正确(如在Spark中)。

额外:我对Spark API的意见

我的观点是,如果在Spark中完全删除了术语fold,则可以避免混淆。至少spark在他们的文档中有一个注释:

  

这与为其实现的折叠操作略有不同   Scala等函数式语言中的非分布式集合。

答案 1 :(得分: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

答案 2 :(得分:2)

Scalding的另一个不同之处是在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始终是一种好习惯。

答案 3 :(得分:2)

Apache Spark中的

fold与未分发的集合上的fold不同。实际上it requires commutative function产生确定性结果:

  

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

has been shown Mishael Rosenthal Make42 his commentIt's been suggested中建议。

Structure of fold观察到的行为与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)

说明:

对于RDD

as structure of reduce

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,

reduceLeft

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时忽略分区顺序,导致需要交换功能。

foldPartitionreducePartition在处理顺序方面是有效的,并且foldLeftTraversableOnce在{{3}}上有效地(通过继承和授权)实现。

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