Почему Scala и фреймворки, такие как Spark и Scalding, имеют как reduce
, так и foldLeft
? Итак, какая разница между reduce
и fold
?
Разница между сокращением и foldLeft/fold в функциональном программировании (в частности, API Scala и Scala)?
Ответ 1
уменьшить vs foldLeft
Большая разница, не упомянутая в любом другом ответе stackoverflow, относящемся к этой теме, заключается в том, что reduce
следует предоставить коммутативный моноид, т.е. операцию, которая является коммутативной и ассоциативной. Это означает, что операция может быть распараллелена.
Это различие очень важно для Big Data/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/математическим корням, и немного сложнее объяснить. Сокращение определяется формально как часть парадигмы MapReduce, которая касается упорядоченных коллекций (мультимножеств), Fold формально определяется в терминах рекурсии (см. Катаморфизм) и, таким образом, принимает структуру/последовательность в коллекции.
В Scalding нет метода fold
, потому что в рамках (строгой) модели программирования Map Reduce мы не можем определить fold
, потому что куски не имеют упорядочения, а fold
требуется ассоциативность, а не коммутативность.
Проще говоря, reduce
работает без порядка кумуляции, fold
требует порядка кумуляции, и именно этот порядок кумуляции требует нулевого значения NOT существования нулевого значения, которое их отличает. Строго говоря, reduce
должен работать с пустой коллекцией, потому что ее нулевое значение может быть выведено путем принятия произвольного значения x
, а затем решения x op y = x
, но это не работает с некоммутативной операцией, поскольку может существовать левое и правое нулевое значение, отличные (т.е. x op y != y op x
). Конечно, Scala не утруждает себя тем, что это нулевое значение, так как это потребует выполнения некоторой математики (которая, вероятно, невычислима), поэтому просто генерирует исключение.
Кажется (как это часто бывает в этимологии), что этот оригинальный математический смысл был утрачен, поскольку единственным очевидным различием в программировании является подпись. В результате reduce
стал синонимом fold
, вместо того, чтобы сохранить его оригинальное значение из MapReduce. Теперь эти термины часто используются взаимозаменяемо и ведут себя одинаково в большинстве реализаций (игнорируя пустые коллекции). Странность усугубляется особенностями, как в Искры, которые мы сейчас рассмотрим.
Таким образом, Spark имеет fold
, но порядок, в котором суб-результаты (по одному для каждого раздела) объединены (на момент написания), является тем же самым порядком, в котором выполняются задачи, и, следовательно, не является детерминированным. Благодаря @CafeFeed, указав, что fold
использует runJob
, который после прочтения кода понял, что он не детерминирован. Дальнейшая путаница создается Спарком, имеющим treeReduce
, но не treeFold
.
Заключение
Существует разница между reduce
и fold
даже при применении к непустым последовательностям. Первый определяется как часть парадигмы программирования MapReduce для коллекций с произвольным порядком (http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf), и следует предположить, что операторы также являются коммутативными к тому, чтобы быть ассоциативным, чтобы дать детерминированные результаты. Последнее определяется в терминах catomorphisms и требует, чтобы коллекции имели понятие последовательности (или определены рекурсивно, как связанные списки), поэтому не требуют коммутативных операторов.
На практике из-за нематического характера программирования reduce
и fold
имеют тенденцию вести себя одинаково, либо правильно (например, в Scala), либо неправильно (например, в Spark).
Дополнительно: мое мнение о API-интерфейсе Spark
Мое мнение заключается в том, что путаницы можно было бы избежать, если бы использование слова fold
было полностью опущено в Spark. По крайней мере, у искры есть заметка в их документации:
Это ведет себя несколько иначе, чем операции сброса, реализованные для нераспределенные коллекции в функциональных языках типа Scala.
Ответ 2
Если я не ошибаюсь, хотя API Spark не требует этого, 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("")(_ + _))
}
Распечатка:
АБВГДЕЖЗИКЛМНОПРСТУФХЧШЭЮЯ
abcghituvjklmwxyzqrsdefnop
defghinopjklmqrstuvabcwxyz
Ответ 3
Еще одно отличие для Scalding - использование комбинаторов в 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 }
}
Всегда правильно определять свои операции как моноиды в Scalding.
Ответ 4
fold
в Apache Spark не совпадает с fold
для нераспределенных коллекций. Фактически он требует коммутативной функции для получения детерминированных результатов:
Это ведет себя несколько иначе, чем операции свертки, реализованные для нераспределенных коллекции в функциональных языках типа Scala. Эта операция сложения может быть применена к разделов индивидуально, а затем сворачивать эти результаты в конечный результат, а не применяйте сгиб к каждому элементу последовательно в некотором определенном порядке. Для функций которые не являются коммутативными, результат может отличаться от результата складывания, применяемого к нераспределенная коллекция.
Этот был показан Мишалем Розенталем и предложен 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
на TraversableOnce
.
Заключение: fold
на RDD не может зависеть от порядка кусков и требует коммутативности и ассоциативности.