Разница между reduce и foldLeft/fold в функциональном программировании (особенно Scala и Scala APIs)?
Почему Scala и фреймворки, такие как Spark и Scalding, имеют оба reduce
и foldLeft
? Так в чем же тогда разница между reduce
и fold
?
4 ответов
уменьшить vs foldLeft
большая большая разница, не упомянутая в любом другом ответе stackoverflow, относящемся к этой теме, заключается в том, что reduce
должно быть задано коммутативным моноидом, т. е. операция является коммутативной и ассоциативной. Это означает, что операция может быть распараллелен.
это различие очень важно для больших данных / MPP / распределенных вычислений, и вся причина, почему reduce
вообще существует. Коллекция может быть порубленным и reduce
может работать на каждом блоке, то reduce
может работать на результатах каждого куска-на самом деле уровень чанкинга не должен останавливаться на одном уровне глубоко. Мы могли бы порубить каждый кусок. Вот почему суммирование целых чисел в списке равно O (log N), если задано бесконечное число процессоров.
если вы просто посмотрите на подписи, нет оснований для reduce
существовать, потому что вы можете достичь всего, что можете с reduce
С foldLeft
. Функциональность foldLeft
больше, чем функциональность reduce
.
но вы не можете распараллелить foldLeft
, поэтому его время выполнения всегда O (N) (даже если вы вводите коммутативный моноид). Это потому, что предполагается, что операция не коммутативный моноид, и поэтому кумулированное значение будет вычислено серией последовательных агрегаций.
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
уменьшить vs fold
теперь это, где он становится немного ближе к FP / математическим корням и немного сложнее объяснить. Reduce определяется формально как часть MapReduce парадигма, которая имеет дело с упорядоченными коллекциями (мультисетями), формально определяется в терминах рекурсии (см. катаморфизм) и, таким образом, предполагает структуру / последовательность коллекций.
нет fold
метод в ошпаривании, потому что под (строгой) картой уменьшить модель программирования мы не можем определить fold
потому что куски не имеют заказа и fold
требуется только ассоциативность, а не коммутативность.
просто reduce
работает без приказа кумуляция,fold
требует порядка кумуляции, и именно этот порядок кумуляции требует нулевого значения, а не существования нулевого значения, которое их отличает. Строго говоря reduce
должны работа над пустой коллекцией, потому что ее нулевое значение можно вывести, взяв произвольное значение x
а затем решением x op y = x
, но это не работает с некоммутативной операцией, поскольку может существовать левое и правое нулевое значение, которые различны (т. е. x op y != y op x
). Конечно, Scala не беспокоится о том, что это нулевое значение, поскольку для этого потребуется выполнить некоторую математику (которая, вероятно, не вычисляется), поэтому просто выдает исключение.
fold
, а не сохранять его первоначальное значение от В MapReduce. Теперь эти термины часто используются взаимозаменяемо и ведут себя одинаково в большинстве реализаций (игнорируя пустые коллекции). Странность усугубляется особенностями, как в Spark, которые мы сейчас рассмотрим.
Так Что Искры тут есть fold
, но порядок, в котором результаты sub (по одному для каждого раздела) объединяются (на момент написания), является тем же порядком, в котором выполняются задачи - и, следовательно, недетерминированным. Спасибо @CafeFeed за указание это fold
использует runJob
, который после прочтения кода я понял, что это недетерминированный. Дальнейшая путаница создается искрой, имеющей treeReduce
а не treeFold
.
вывод
есть разница между reduce
и fold
даже при применении к непустым последовательностям. Первый определяется как часть парадигмы программирования MapReduce на коллекциях с произвольным порядком (http://theory.stanford.edu / ~Сергей / документы / soda10-mrc.pdf) и следует предположить, что операторы являются коммутативными в дополнение к ассоциативным, чтобы дать детерминированные результаты. Последний определяется в терминах катоморфизмов и требует, чтобы коллекции имели понятие последовательности (или определялись рекурсивно, как связанные списки), поэтому не требуют коммутативных операторов.
на практике из-за нематематической природы программирования, reduce
и fold
как правило, вести себя одинаково, либо правильно (как в Scala), либо неправильно (как в Spark).
Extra: мое мнение о Spark API
мое мнение, что путаницы можно было бы избежать, если использовать термин fold
был полностью сброшен в Spark. По крайней мере, у spark есть примечание в их документации:
это ведет себя несколько иначе, чем операции сгиба, реализованные для нераспределенные коллекции на функциональных языках, таких как Скала.
Если я не ошибаюсь, даже если Spark API не требует этого, fold также требует, чтобы 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
еще одним отличием для ошпаривания является использование комбинаторов в Hadoop.
представьте, что ваша операция является коммутативным моноидом, с уменьшить он будет применяться на стороне карты также вместо перетасовки / сортировки всех данных в редукторы. С метод использовать-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 }
}
всегда рекомендуется определять ваши операции как моноиды при ошпаривании.
fold
в Apache Spark не то же самое, что fold
на не-распределенных коллекциях. На самом деле требуется коммутативная функция для получения детерминированного результата:
это ведет себя несколько иначе, чем операции сгиба, реализованные для не распределенных коллекции на функциональных языках, таких как Scala. Эта деятельность створки может быть приложена к разделы по отдельности, а затем сложить эти результаты в конечный результат, а не применить сложите каждый элемент последовательно в определенном порядке. Для функций которые не являются коммутативными, результат может отличаться от результата сгиба, примененного к non-distributed collection.
этой показали by Мишель Розенталь и предложил 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)
пояснил:
структура fold
для RDD
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
}
тот же в структуре reduce
для RDD:
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
on TraversableOnce
.
вывод: fold
на RDD не может зависеть от порядка кусков и потребностей коммутативность и ассоциативность.