web-dev-qa-db-fra.com

Différence entre réduction et foldLeft / fold dans la programmation fonctionnelle (en particulier Scala et Scala API))?

Pourquoi Scala et les frameworks tels que Spark et Scalding ont-ils reduce et foldLeft?? Alors quelle est la différence entre reduce et fold?

92
samthebest

réduire vs foldLeft

Une grosse différence, clairement mentionnée dans aucune autre réponse de stackoverflow concernant ce sujet, est que reduce devrait recevoir un monoïde commutatif, c’est-à-dire une opération à la fois commutative et active. associatif. Cela signifie que l'opération peut être mise en parallèle.

Cette distinction est très importante pour Big Data/MPP/calcul distribué, et constitue la raison même pour laquelle reduce existe même. La collection peut être découpée et le reduce peut fonctionner sur chaque bloc, puis le reduce peut agir sur les résultats de chaque bloc - en fait, le niveau de fragmentation ne doit pas s'arrêter profondément. Nous pourrions aussi couper chaque morceau. C'est pourquoi la somme des nombres entiers dans une liste est O (log N) si un nombre infini de CPU lui sont attribués.

Si vous ne faites que regarder les signatures, il n’ya aucune raison que reduce existe, car vous pouvez obtenir tout ce que vous pouvez avec reduce avec un foldLeft. La fonctionnalité de foldLeft est supérieure à la fonctionnalité de reduce.

Mais vous ne pouvez pas paralléliser un foldLeft, de sorte que son exécution est toujours O(N) (même si vous alimentez dans un monoïde commutatif). Cela est dû au fait que l'opération est supposée pas un monoïde commutatif. La valeur cumulée sera donc calculée par une série d'agrégations séquentielles.

foldLeft n'assume pas la commutativité ni l'associativité. C'est l'associativité qui permet de découper la collection et la commutativité qui facilite le cumul, car l'ordre n'a pas d'importance (le choix d'agréger chacun des résultats de chaque bloc n'a donc pas d'importance). La commutativité à proprement parler n'est pas nécessaire pour la parallélisation, par exemple les algorithmes de tri distribué, cela facilite simplement la logique car vous n'avez pas besoin de donner un ordre à vos morceaux.

Si vous consultez la documentation Spark de reduce, elle indique 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 simplement un cas particulier 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 fois

Maintenant, c’est là que cela se rapproche un peu des racines mathématiques FP /, et un peu plus délicat à expliquer. Réduire est défini formellement dans le cadre du paradigme MapReduce, qui traite des collections sans ordre (multisets). Fold est défini formellement en termes de récursivité (voir catamorphisme) et assume donc une structure/séquence des collections.

Il n'y a pas de méthode fold dans Scalding car, dans le modèle de programmation (strict) MapRepse, nous ne pouvons pas définir fold car les morceaux n'ont pas d'ordre et que fold requiert uniquement une associativité, pas commutativité.

En termes simples, reduce fonctionne sans ordre de cumul, fold nécessite un ordre de cumul et c’est cet ordre de cumul qui nécessite une valeur nulle PAS l’existence de la valeur zéro qui les distingue. Strictement parlant, reduce devrait travailler sur une collection vide, car 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 des valeurs zéro gauche et droite distinctes (c'est-à-dire x op y != y op x). Bien sûr, Scala ne se donne pas la peine de déterminer la valeur zéro car cela nécessiterait quelques calculs mathématiques (qui ne sont probablement pas calculables).

Il semble (comme c'est souvent le cas en étymologie) que cette signification mathématique originale ait é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 conserver sa signification originale de MapReduce. Maintenant, ces termes sont souvent utilisés de manière interchangeable et se comportent de la même manière dans la plupart des implémentations (en ignorant les collections vides). L'étrangeté est exacerbée par des particularités, comme dans Spark, que nous allons maintenant aborder.

Donc Spark fait ont un fold, mais l'ordre dans lequel les sous-résultats (un pour chaque partition) sont combinés (au moment de l'écriture) est le même ordre dans lequel les tâches sont accomplies - et donc non déterministe. Merci à @CafeFeed d'avoir signalé que fold utilise runJob, ce qui, après avoir lu le code, m'a rendu compte que ce n'est pas déterministe. Une autre confusion est créée par le fait que Spark a un treeReduce mais pas de treeFold.

Conclusion

Il y a une différence entre reduce et fold même lorsqu'elle est appliquée à des séquences non vides. Le premier est défini 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 il faut supposer les opérateurs sont commutatifs en plus d'être associatifs pour donner des résultats déterministes. Ce dernier est défini en termes de catomorphismes et nécessite que les collections aient une notion de séquence (ou soient définies de manière récursive, comme des listes chaînées), ne nécessitant donc pas d'opérateurs commutatifs.

En 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) ou incorrectement (comme dans Spark).

Extra: Mon opinion sur l'API Spark

Mon opinion est que cette confusion serait évitée si l'utilisation du terme fold était complètement supprimée dans Spark. Au moins spark a une note dans sa documentation:

Cela se comporte quelque peu différemment des opérations de pliage implémentées pour les collections non distribuées dans des langages fonctionnels tels que Scala.

249
samthebest

Si je ne me trompe pas, même si l'API Spark ne l'exige pas, fold nécessite également que le f soit commutatif. Parce que l'ordre dans lequel les partitions seront agrégées n'est pas assuré. Pour 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("")(_ + _))
}  

Imprimer:

abcdefghijklmnopqrstuvwxyz

abcghituvjklmwxyzqrsdefnop

defghinopjklmqrstuvabcwxyz

10
Mishael Rosenthal

Une autre différence pour Scalding est l'utilisation de combineurs dans Hadoop.

Imaginez que votre opération soit monoïde commutative, avec réduire elle sera également appliquée du côté de la carte au lieu de mélanger/trier toutes les données dans des 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 }
}

Il est toujours bon de définir vos opérations comme monoïdes dans Scalding.

2
morazow

fold dans Apache Spark n'est pas identique à fold sur des collections non distribuées. En fait il faut une fonction commutative pour produire résultats déterministes:

Cela se comporte quelque peu différemment des opérations de pliage implémentées pour les collections non distribuées dans des langages fonctionnels tels que Scala. Cette opération de pliage peut être appliquée aux partitions individuellement, puis incorporer ces résultats au résultat final, plutôt que d'appliquer le pli à chaque élément de manière séquentielle dans un ordre défini. Pour les fonctions non commutatives, le résultat peut différer de celui d'un repli appliqué à une collection non distribuée.

Ceci a été montré par Mishael Rosenthal et suggéré par Make42 dans son commentaire .

Il a été suggéré que le comportement observé est lié à HashPartitioner alors qu'en fait parallelize ne mélange 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)

A expliqué:

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 comme 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"))
}

runJob est exécuté sans tenir compte de l'ordre des partitions et nécessite donc une fonction commutative.

foldPartition et reducePartition sont équivalents en termes d'ordre de traitement et effectivement (par héritage et délégation) mis en œuvre par reduceLeft et foldLeft sur TraversableOnce .

Conclusion: fold sur RDD ne peut pas dépendre de l'ordre des morceaux et des besoins de la commutativité et de l'associativité .

2
user6022341