J'ai cherché et je ne trouve pas d'exemple ou de discussion sur la fonction aggregate
dans Scala que je peux comprendre. Cela semble assez puissant.
Cette fonction peut-elle être utilisée pour réduire les valeurs des tuples pour créer une collection de type multimap? Par exemple:
val list = Seq(("one", "i"), ("two", "2"), ("two", "ii"), ("one", "1"), ("four", "iv"))
Après avoir appliqué l'agrégat:
Seq(("one" -> Seq("i","1")), ("two" -> Seq("2", "ii")), ("four" -> Seq("iv"))
Pouvez-vous également donner un exemple de paramètres z
, segop
et combop
? Je ne sais pas exactement ce que font ces paramètres.
La fonction d'agrégation ne fait pas cela (sauf que c'est une fonction très générale et qu'elle pourrait être utilisée pour cela). Vous voulez groupBy
. Près d'au moins. Lorsque vous commencez par une Seq[(String, String)]
et que vous regroupez en prenant le premier élément du tuple (qui est (String, String) => String)
, Il renvoie une Map[String, Seq[(String, String)]
). Vous devez ensuite ignorer le premier paramètre des valeurs Seq [String, String)].
Donc
list.groupBy(_._1).mapValues(_.map(_._2))
Là, vous obtenez une Map[String, Seq[(String, String)]
. Si vous voulez un Seq
au lieu de Map
, appelez toSeq
sur le résultat. Je ne pense pas que vous ayez une garantie sur la commande dans le Seq résultant
L'agrégat est une fonction plus difficile.
Envisagez d'abord de réduire la gauche et de réduire la droite. Soit as
une séquence non vide as = Seq(a1, ... an)
d'éléments de type A
et f: (A,A) => A
soit un moyen de combiner deux éléments de type A
en un seul. Je le noterai comme un opérateur binaire @
, a1 @ a2
Plutôt que f(a1, a2)
. as.reduceLeft(@)
calculera (((a1 @ a2) @ a3)... @ an)
. reduceRight
mettra les parenthèses dans l'autre sens, (a1 @ (a2 @... @ an))))
. Si @
Se trouve être associatif, on ne se soucie pas des parenthèses. On pourrait le calculer comme (a1 @... @ ap) @ (ap+1 @...@an)
(Il y aurait aussi des paranthèses à l'intérieur des 2 grandes paranthèses, mais ne nous en soucions pas). Ensuite, on pourrait faire les deux parties en parallèle, tandis que le bracketing imbriqué dans ReduceLeft ou ReduceRight force un calcul entièrement séquentiel. Mais le calcul parallèle n'est possible que lorsque @
Est connu pour être associatif et que la méthode ReduceLeft ne peut pas le savoir.
Pourtant, il pourrait y avoir la méthode reduce
, dont l'appelant serait responsable de s'assurer que l'opération est associative. Ensuite, reduce
ordonnerait les appels comme bon lui semble, éventuellement en parallèle. En effet, il existe une telle méthode.
Il existe cependant une limitation avec les différentes méthodes de réduction. Les éléments de la Seq ne peuvent être combinés qu'à un résultat du même type: @
Doit être (A,A) => A
. Mais on pourrait avoir le problème plus général de les combiner en un B
. On commence par une valeur b
de type B
, et on la combine avec tous les éléments de la séquence. L'opérateur @
Est (B,A) => B
, Et on calcule (((b @ a1) @ a2) ... @ an)
. foldLeft
fait cela. foldRight
fait la même chose mais en commençant par an
. Là, l'opération @
N'a aucune chance d'être associative. Quand on écrit b @ a1 @ a2
, Cela doit signifier (b @ a1) @ a2
, Car (a1 @ a2)
Serait mal tapé. Donc, foldLeft et foldRight doivent être séquentiels.
Supposons cependant que chaque A
puisse être transformé en B
, écrivons-le avec !
, a!
Est de type B
. Supposons en outre qu'il existe une opération +
(B,B) => B
, Et que @
Soit tel que b @ a
Soit en fait b + a!
. Plutôt que de combiner des éléments avec @, on pourrait d'abord les transformer tous en B avec !
, Puis les combiner avec +
. Ce serait as.map(!).reduceLeft(+)
. Et si +
Est associatif, cela peut être fait avec réduire, et ne pas être séquentiel: as.map (!). Réduire (+). Il pourrait y avoir une méthode hypothétique comme.associativeFold (b,!, +).
L'agrégat est très proche de cela. Il se peut cependant qu'il existe un moyen plus efficace d'implémenter b@a
Que b+a!
Par exemple, si le type B
est List[A]
, Et b @ a est a :: b, alors a!
sera a::Nil
et b1 + b2
sera b2 ::: b1
. a :: b est bien meilleur que (a :: Nil) ::: b. Pour bénéficier de l'associativité, mais toujours utiliser @
, On divise d'abord b + a1! + ... + an!
En (b + a1! + ap!) + (ap+1! + ..+ an!)
, Puis on recommence à utiliser @
Avec (b @ a1 @ an) + (ap+1! @ @ an)
. Il faut encore le! sur ap + 1, car il faut commencer par certains b. Et le + est toujours nécessaire aussi, apparaissant entre les parenthèses. Pour ce faire, as.associativeFold(!, +)
peut être remplacé par as.optimizedAssociativeFold(b, !, @, +)
.
Retour à +
. +
Est associatif, ou de manière équivalente, (B, +)
Est un semi-groupe. En pratique, la plupart des semigroupes utilisés en programmation sont également des monoides, c'est-à-dire qu'ils contiennent un élément neutre z
(pour zéro) en B, de sorte que pour chaque b
, z + b
= b + z
= b
. Dans ce cas, l'opération !
Qui a du sens sera probablement a! = z @ a
. De plus, comme z est un élément neutre b @ a1 ..@ an = (b + z) @ a1 @ an
qui est b + (z + a1 @ an)
. Il est donc toujours possible de démarrer l'agrégation avec z. Si b
est souhaité à la place, vous faites b + result
À la fin. Avec toutes ces hypothèses, nous pouvons faire as.aggregate(z, @, +)
. C'est ce que fait aggregate
. @
Est l'argument seqop
(appliqué dans une séquencez @ a1 @ a2 @ ap
), Et +
Est combop
( appliqué aux résultats déjà partiellement combinés, comme dans (z + a1@...@ap) + (z + ap+1@...@an)
).
Pour résumer, as.aggregate(z)(seqop, combop)
calcule la même chose que as.foldLeft(z)( seqop)
à condition que
(B, combop, z)
Est un monoïdeseqop(b,a) = combop(b, seqop(z,a))
l'implémentation d'agrégat peut utiliser l'associativité de combop pour regrouper les calculs comme bon lui semble (sans permuter les éléments cependant, + ne doit pas être commutatif, ::: ne l'est pas) Il peut les exécuter en parallèle.
Enfin, la résolution du problème initial à l'aide de aggregate
est laissée au lecteur comme exercice. Un conseil: implémentez en utilisant foldLeft
, puis trouvez z
et combo
qui satisferont aux conditions énoncées ci-dessus.
Voyons voir si un art ascii n'aide pas. Considérez la signature de type de aggregate
:
def aggregate [B] (z: B)(seqop: (B, A) ⇒ B, combop: (B, B) ⇒ B): B
Notez également que A
fait référence au type de la collection. Disons que nous avons 4 éléments dans cette collection, alors aggregate
pourrait fonctionner comme ceci:
z A z A z A z A
\ / \ /seqop\ / \ /
B B B B
\ / combop \ /
B _ _ B
\ combop /
B
Voyons un exemple pratique de cela. Disons que j'ai une GenSeq("This", "is", "an", "example")
, et je veux savoir combien de caractères il y a dedans. Je peux écrire ce qui suit:
Notez l'utilisation de par
dans l'extrait de code ci-dessous. La deuxième fonction passée à l'agrégat est ce qui est appelé après le calcul des séquences individuelles. Scala ne peut le faire que pour les ensembles qui peuvent être parallélisés.
import scala.collection.GenSeq
val seq = GenSeq("This", "is", "an", "example")
val chars = seq.par.aggregate(0)(_ + _.length, _ + _)
Donc, tout d'abord, il calculerait ceci:
0 + "This".length // 4
0 + "is".length // 2
0 + "an".length // 2
0 + "example".length // 7
Ce qu'il fait ensuite ne peut pas être prévu (il y a plus d'une façon de combiner les résultats), mais il pourrait le faire (comme dans l'art ascii ci-dessus):
4 + 2 // 6
2 + 7 // 9
À ce stade, il conclut par
6 + 9 // 15
ce qui donne le résultat final. Maintenant, c'est un peu similaire dans la structure à foldLeft
, mais il a une fonction supplémentaire (B, B) => B
, Qui n'a pas de pli. Cette fonction lui permet cependant de travailler en parallèle!
Considérons, par exemple, que chacun des quatre calculs initiaux est indépendant les uns des autres et peut être effectué en parallèle. Les deux suivants (résultant en 6 et 9) peuvent être démarrés une fois que leurs calculs dont ils dépendent sont terminés, mais ces deux peuvent aussi s'exécuter en parallèle.
Les 7 calculs, parallélisés comme ci-dessus, pourraient prendre aussi peu que le même temps 3 calculs en série.
En fait, avec une si petite collection, le coût de synchronisation du calcul serait suffisamment élevé pour effacer tous les gains. De plus, si vous pliez cela, cela ne prendrait que 4 calculs au total. Cependant, une fois que vos collections s'agrandissent, vous commencez à voir de réels gains.
Considérez, d'autre part, foldLeft
. Parce qu'il n'a pas de fonction supplémentaire, il ne peut paralléliser aucun calcul:
(((0 + "This".length) + "is".length) + "an".length) + "example".length
Chacune des parenthèses internes doit être calculée avant que la parenthèse externe puisse continuer.
La signature d'une collection avec des éléments de type A est:
def aggregate [B] (z: B)(seqop: (B, A) ⇒ B, combop: (B, B) ⇒ B): B
z
est un objet de type B agissant comme un élément neutre. Si vous voulez compter quelque chose, vous pouvez utiliser 0, si vous voulez construire une liste, commencez par une liste vide, etc.segop
est analogue à la fonction que vous passez aux méthodes fold
. Il prend deux arguments, le premier est du même type que l'élément neutre que vous avez passé et représente les éléments qui ont déjà été agrégés lors de l'itération précédente, le second est l'élément suivant de votre collection. Le résultat doit également être de type B
.combop
: est une fonction combinant deux résultats en un.Dans la plupart des collections, l'agrégat est implémenté dans TraversableOnce
comme:
def aggregate[B](z: B)(seqop: (B, A) => B, combop: (B, B) => B): B
= foldLeft(z)(seqop)
Ainsi combop
est ignoré. Cependant, cela a du sens pour les collections parallèles, car seqop
sera d'abord appliqué localement en parallèle, puis combop
sera appelé pour terminer l'agrégation.
Donc, pour votre exemple, vous pouvez d'abord essayer avec un pli:
val seqOp =
(map:Map[String,Set[String]],Tuple: (String,String)) =>
map + ( Tuple._1 -> ( map.getOrElse( Tuple._1, Set[String]() ) + Tuple._2 ) )
list.foldLeft( Map[String,Set[String]]() )( seqOp )
// returns: Map(one -> Set(i, 1), two -> Set(2, ii), four -> Set(iv))
Ensuite, vous devez trouver un moyen de réduire deux multimaps:
val combOp = (map1: Map[String,Set[String]], map2: Map[String,Set[String]]) =>
(map1.keySet ++ map2.keySet).foldLeft( Map[String,Set[String]]() ) {
(result,k) =>
result + ( k -> ( map1.getOrElse(k,Set[String]() ) ++ map2.getOrElse(k,Set[String]() ) ) )
}
Maintenant, vous pouvez utiliser l'agrégat en parallèle:
list.par.aggregate( Map[String,Set[String]]() )( seqOp, combOp )
//Returns: Map(one -> Set(i, 1), two -> Set(2, ii), four -> Set(iv))
Appliquer la méthode "par" à la liste, en utilisant ainsi la collection parallèle (scala.collection.parallel.immutable.ParSeq) de la liste pour vraiment profiter des processeurs multicœurs. Sans "par", il n'y aura aucun gain de performances puisque l'agrégat ne se fait pas sur la collection parallèle.
aggregate
est comme foldLeft
mais peut être exécuté en parallèle.
Comme facteur manquant dit , la version linéaire de aggregate(z)(seqop, combop)
est équivalente à foldleft(z)(seqop)
. Ceci est cependant peu pratique dans le cas parallèle, où nous aurions besoin de combiner non seulement l'élément suivant avec le résultat précédent (comme dans un pli normal) mais nous voulons diviser l'itérable en sous-itérables sur lesquels nous appelons agrégat et devons combinez-les à nouveau. (Dans l'ordre de gauche à droite mais pas associatif car nous aurions pu combiner les dernières parties avant les premières parties de l'itérable.) Cette re-combinaison en général non triviale, et donc, il faut une méthode (S, S) => S
pour y parvenir.
La définition dans ParIterableLike
est:
def aggregate[S](z: S)(seqop: (S, T) => S, combop: (S, S) => S): S = {
executeAndWaitResult(new Aggregate(z, seqop, combop, splitter))
}
qui utilise en effet combop
.
Pour référence, Aggregate
est défini comme:
protected[this] class Aggregate[S](z: S, seqop: (S, T) => S, combop: (S, S) => S, protected[this] val pit: IterableSplitter[T])
extends Accessor[S, Aggregate[S]] {
@volatile var result: S = null.asInstanceOf[S]
def leaf(prevr: Option[S]) = result = pit.foldLeft(z)(seqop)
protected[this] def newSubtask(p: IterableSplitter[T]) = new Aggregate(z, seqop, combop, p)
override def merge(that: Aggregate[S]) = result = combop(result, that.result)
}
La partie importante est merge
où combop
est appliqué avec deux sous-résultats.
Voici le blog sur la façon dont les agrégats permettent des performances sur le processeur multi-cœurs avec benchmark. http://markusjais.com/scalas-parallel-collections-and-the-aggregate-method/
Voici une vidéo sur les "collections parallèles Scala", conférence de "Scala Days 2011". http://days2011.scala-lang.org/node/138/272
La description sur la vidéo
Collections Scala Parallel
Aleksandar Prokopec
Les abstractions de programmation parallèle deviennent de plus en plus importantes à mesure que le nombre de cœurs de processeur augmente. Un modèle de programmation de haut niveau permet au programmeur de se concentrer davantage sur le programme et moins sur les détails de bas niveau tels que la synchronisation et l'équilibrage de charge. Scala collections parallèles étendent le modèle de programmation du cadre de collecte Scala, fournissant des opérations parallèles sur les ensembles de données. L'exposé décrira l'architecture du cadre de collection parallèle, expliquant leur mise en œuvre et leurs décisions de conception. Des implémentations de collections concrètes telles que des cartes de hachage parallèles et des essais de hachage parallèles seront décrites. Enfin, plusieurs exemples d'applications seront présentés, démontrant le modèle de programmation dans la pratique.
La définition de aggregate
dans TraversableOnce
source est:
def aggregate[B](z: B)(seqop: (B, A) => B, combop: (B, B) => B): B =
foldLeft(z)(seqop)
qui n'est pas différent d'un simple foldLeft
. combop
ne semble être utilisé nulle part. Je suis moi-même confus quant à la finalité de cette méthode.
Juste pour clarifier les explications de ceux qui m'ont précédé, en théorie l'idée est que l'agrégat devrait fonctionner comme ceci, (j'ai changé les noms des paramètres pour les rendre plus clairs):
Seq(1,2,3,4).aggragate(0)(
addToPrev = (prev,curr) => prev + curr,
combineSums = (sumA,sumB) => sumA + sumB)
Devrait logiquement se traduire par
Seq(1,2,3,4)
.grouped(2) // split into groups of 2 members each
.map(prevAndCurrList => prevAndCurrList(0) + prevAndCurrList(1))
.foldLeft(0)(sumA,sumB => sumA + sumB)
Étant donné que l'agrégation et le mappage sont séparés, la liste d'origine pourrait théoriquement être divisée en différents groupes de tailles différentes et s'exécuter en parallèle ou même sur une machine différente. En pratique scala l'implémentation actuelle ne prend pas en charge cette fonctionnalité par défaut mais vous pouvez le faire dans votre propre code.