Différence entre reduce et foldLeft / fold dans la programmation fonctionnelle (particulièrement Scala et Scala APIs)?
pourquoi Scala et les cadres comme Spark et L'ébouillantage ont à la fois reduce
et foldLeft
? Alors, quelle est la différence entre reduce
et fold
?
4 réponses
réduire vs foldLeft
une grande différence, non mentionnée dans aucune autre réponse de débordement de piles relative à ce sujet clairement, est que reduce
doit être donnée un monoïde commutatif , c.-à-d. une opération qui est à la fois commutative et associative. Cela signifie que l'opération peut être parallélisée.
cette distinction est très importante pour le Big Data / MPP / distributed computing, et toute la raison pour laquelle reduce
existe même. La collection peut être découpée et le reduce
peut fonctionner sur chaque morceau, puis le reduce
peut fonctionner sur les résultats de chaque morceau - en fait, le niveau de chunking ne doit pas arrêter un niveau profond. Nous pourrions couper chaque morceau. C'est pourquoi la sommation d'entiers dans une liste est O(log N) Si on lui donne un nombre infini de CPU.
si vous regardez juste les signatures il n'y a aucune raison pour que reduce
existe parce que vous pouvez atteindre tout ce que vous pouvez avec reduce
avec un foldLeft
. La fonctionnalité de foldLeft
est supérieure à celle de reduce
.
mais vous ne pouvez pas mettre en parallèle un foldLeft
, donc son runtime est toujours O(N) (même si vous vous nourrissez dans un monoïde commutatif). C'est parce qu'il est supposé que l'opération est pas un monoïde commutatif et donc la valeur cumulée est calculée par une série d'séquentielle agrégation.
foldLeft
ne suppose ni commutativité ni associativité. C'est l'associativité qui donne la capacité de découper la collection, et c'est la commutativité qui rend le cumul facile parce que l'ordre n'est pas important (donc ce n'est pas important quel ordre pour agréger chacun des résultats de chacun des morceaux). La commutativité à proprement parler n'est pas nécessaire pour la parallélisation, par exemple les algorithmes de tri distribués, elle rend juste la logique plus facile parce que tu n'as pas besoin de donner un ordre à tes morceaux.
si vous avez un regard à la documentation Spark pour reduce
il dit spécifiquement"... opérateur binaire commutatif et associatif "
http://spark.apache.org/docs/1.0.0/api/scala/index.html#org.apache.spark.rdd.RDD
Voici la preuve que reduce
N'est pas seulement un cas spécial de 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
réduire vs pli
maintenant, c'est ici qu'il se rapproche un peu plus des racines mathématiques FP/, et un peu plus difficile à expliquer. Reduce est formellement défini comme faisant partie du paradigme MapReduce, qui traite des collections sans ordre (multisets), le pli est formellement défini en termes de récursion (voir catamorphisme) et suppose donc une structure / séquence aux collections.
il n'y a pas de fold
méthode dans L'ébouillantage parce que sous le (strict) Carte réduire le modèle de programmation nous ne pouvons pas définir fold
parce que les morceaux n'ont pas d'ordre et fold
ne nécessite que l'associativité, pas la commutativité.
dit simplement, reduce
fonctionne sans ordre de cumul, fold
exige un ordre de cumul et c'est cet ordre de cumul qui nécessite une valeur zéro et non l'existence de la valeur zéro qui les distingue. À proprement parler reduce
devrait travaille sur une collection vide, parce que sa valeur zéro peut être déduite en prenant une valeur arbitraire x
puis en résolvant x op y = x
, mais cela ne fonctionne pas avec une opération non commutative car il peut exister une valeur zéro gauche et droite qui sont distinctes (i.e. x op y != y op x
). Bien sûr, Scala ne prend pas la peine de travailler sur ce que cette valeur Zéro est que cela nécessiterait de faire quelques mathématiques (qui sont sans doute uncomputable), donc jette juste une exception.
il semble (comme c'est souvent le cas en étymologie) que cette signification mathématique originale a été perdue, puisque la seule différence évidente dans la programmation est la signature. Le résultat est que reduce
est devenu un synonyme de fold
, plutôt que de préserver son sens original de MapReduce. Maintenant, ces termes sont souvent utilisés de façon interchangeable et se comportent de la même façon dans la plupart des implémentations (en ignorant les collections vides). La bizarrerie est exacerbée par des particularités, comme dans Spark, qui nous allons maintenant aborder.
Donc Étincelle ne avoir une fold
, mais l'ordre dans lequel les sous-résultats (un pour chaque partition) sont combinées (au moment de l'écriture) est du même ordre dans lequel les tâches sont terminées - et donc non-déterministe. Merci à @CafeFeed d'avoir souligné que fold
utilise runJob
, qui après avoir lu le code, j'ai réalisé que c'est non déterministe. Une autre confusion est créée par L'étincelle ayant un treeReduce
mais pas treeFold
.
Conclusion
il y a une différence entre reduce
et fold
même lorsqu'elle est appliquée à des séquences non vides. La première est définie comme faisant partie du paradigme de programmation MapReduce sur les collections avec un ordre arbitraire ( http://theory.stanford.edu / ~ sergei / papers / soda10-mrc.pdf ) et on devrait supposer que les opérateurs sont commutatifs en plus d'être associatifs à donner résultats déterministes. Ce dernier est défini en termes de catomorphismes et exige que les collections aient une notion de séquence (ou soient définies de façon récursive, comme des listes liées), donc ne nécessitent pas d'opérateurs commutatifs.
dans la pratique en raison de la nature non mathématique de la programmation, reduce
et fold
ont tendance à se comporter de la même manière, soit correctement (comme dans Scala), soit incorrectement (comme dans Spark).
Extra: mon avis sur la Spark API
mon opinion est que la confusion serait évitée si l'utilisation du terme fold
était complètement abandonnée. Au moins spark a une note dans sa documentation:
cela se comporte un peu différemment des opérations de plis mises en œuvre pour collections non distribuées dans des langues fonctionnelles Comme le Scala.
si Je ne me trompe pas, même si L'API Spark ne l'exige pas, fold exige aussi que le f soit commutatif. Parce que l'ordre dans lequel les partitions seront agrégées n'est pas assurée. Par exemple, dans le code suivant, seule la première impression est triée:
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("")(_ + _))
}
impression: l'Impression
abcdefghijklmnopqrstuvwxyz
abcghituvjklmwxyzqrsdefnop
defghinopjklmqrstuvabcwxyz
une autre différence pour L'ébouillantage est l'utilisation de peignoirs dans Hadoop.
Imaginez que votre opération est monoïde commutatif, avec réduire il sera appliqué sur le côté de la carte aussi au lieu de mélanger/trier toutes les données aux réducteurs. Avec foldLeft ce n'est pas le cas.
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 }
}
C'est toujours une bonne pratique de définir vos opérations de monoïde Bouillante.
fold
Apache Spark n'est pas la même chose que fold
non-distribué des collections. En fait il nécessite fonction commutative pour produire des résultats déterministes:
ce comportement est quelque peu différent des opérations de pli mises en œuvre pour les collections en langues fonctionnelles Comme le Scala. Cette opération de pliage peut être appliquée à cloisons individuellement, puis plier ces résultats dans le résultat final, plutôt que de appliquez le pli à chaque élément de façon séquentielle dans un ordre défini. Pour les fonctions qui ne sont pas commutatifs, le résultat peut différer de celui d'un pli appliqué à un collection non distribuée.
Ce a été montré par Mishael Rosenthal proposé par Make42 dans son commentaire .
il a été suggéré que le comportement observé est lié à HashPartitioner
alors qu'en fait parallelize
ne bat pas et n'utilise pas 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)
expliquée:
Structure de fold
pour 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
}
est le même que la structure de reduce
pour 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"))
}
où runJob
est effectué avec le mépris de l'ordre de partition et les résultats ont besoin de la fonction commutative.
foldPartition
et reducePartition
sont équivalentes en termes d'ordre de la transformation et de manière efficace (par héritage et délégation) mis en œuvre par reduceLeft
et foldLeft
TraversableOnce
.
Conclusion: fold
CA ne peut pas compter sur l'ordre des morceaux et des besoins la commutativité et l'associativité .