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

函数式编程中reduce和foldLeft/fold的区别(特别是Scala和scalaapi)?,scala,functional-programming,reduce,fold,scalding,Scala,Functional Programming,Reduce,Fold,Scalding,为什么Scala和Spark和Spark之类的框架都有reduce和foldLeft?那么reduce和fold之间有什么区别呢?reduce和foldLeft 与本主题相关的任何其他stackoverflow答案中都没有明确提到的一个大区别是,reduce应该给出一个交换幺半群,即一个既可交换又可结合的运算。这意味着操作可以并行化 这种区别对于大数据/MPP/分布式计算非常重要,甚至存在reduce的全部原因。集合可以被切碎,reduce可以对每个区块进行操作,然后reduce可以对每个区块的

为什么Scala和Spark和Spark之类的框架都有
reduce
foldLeft
?那么
reduce
fold
之间有什么区别呢?

reduce和foldLeft 与本主题相关的任何其他stackoverflow答案中都没有明确提到的一个大区别是,
reduce
应该给出一个交换幺半群,即一个既可交换又可结合的运算。这意味着操作可以并行化

这种区别对于大数据/MPP/分布式计算非常重要,甚至存在
reduce
的全部原因。集合可以被切碎,
reduce
可以对每个区块进行操作,然后
reduce
可以对每个区块的结果进行操作-事实上,区块的层次不需要停止一个层次的深度。我们也可以把每一块都切碎。这就是为什么在给定无限多个CPU的情况下,列表中的整数求和是O(logn)

如果你只看签名,就没有理由存在
reduce
,因为你可以用
reduce
foldLeft
实现你所能实现的一切。
foldLeft
的功能大于
reduce
的功能

但是不能并行化
foldLeft
,因此它的运行时间总是O(N)(即使你输入了一个交换幺半群)。这是因为它假设操作不是交换幺半群,因此累积值将由一系列顺序聚合计算

foldLeft
不假定交换性或结合性。正是关联性提供了分割集合的能力,正是交换性使累积变得容易,因为顺序并不重要(因此聚合每个数据块的每个结果的顺序并不重要)。严格来说,并行化不需要交换性,例如分布式排序算法,它只是使逻辑更容易,因为您不需要给块排序

如果你看一下
reduce
的Spark文档,它特别指出“…交换和关联二进制运算符”

这里证明了
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范式处理无序集合(Multiset),Fold被正式定义为递归(参见退化),因此假定集合的结构/序列

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

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

似乎(在词源学中经常如此)由于编程中唯一明显的区别是签名,这种原始的数学意义已经丢失。结果是
reduce
已成为
fold
的同义词,而不是从MapReduce中保留其原始意义。现在这些术语经常互换使用,在大多数情况下表现相同怪诞(忽略空洞的收藏)。怪诞会因我们现在要讨论的特殊性而加剧,比如《星火》

因此Spark确实有一个折叠,但是子结果(每个分区一个)的组合顺序(在编写时)与任务完成的顺序相同,因此是不确定的。感谢@cafeed指出
fold
使用
runJob
,在阅读代码后,我意识到它是不确定的。Spark使用
treeReduce
但没有
treeFold
会造成进一步的混淆

结论 即使应用于非空序列,
reduce
fold
之间也存在差异。前者被定义为任意顺序集合的MapReduce编程范例的一部分()除了具有关联性之外,还应该假设操作符是可交换的,以给出确定性结果。后者是根据catomorphism定义的,要求集合具有序列的概念(或递归定义,如链表),因此不需要交换操作符

实际上,由于编程的非主题性,
reduce
fold
往往以相同的方式工作,要么正确(如Scala),要么错误(如Spark)

额外:我对Spark API的看法 我的意见是,如果在Spark中完全不使用术语
fold
,可以避免混淆。至少Spark在其文档中有一个注释:

这与为实现的折叠操作有些不同 函数式语言中的非分布式集合
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 }
}
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("")(_ + _))
}  
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)
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
}
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"))
}