Разница между 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 не беспокоится о том, что это нулевое значение, поскольку для этого потребуется выполнить некоторую математику (которая, вероятно, не вычисляется), поэтому просто выдает исключение.

reduce стал синонимом 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 не может зависеть от порядка кусков и потребностей коммутативность и ассоциативность.