J'ai une liste de cartes [chaîne, double] et j'aimerais fusionner leur contenu dans une seule carte [chaîne, double]. Comment dois-je faire cela d'une manière idiomatique? J'imagine que je devrais être capable de faire cela avec un pli. Quelque chose comme:
val newMap = Map[String, Double]() /: listOfMaps { (accumulator, m) => ... }
De plus, j'aimerais gérer les collisions de clés de manière générique. C'est-à-dire que si j'ajoute une clé à la carte existante, je devrais pouvoir spécifier une fonction qui renvoie un Double (dans ce cas) et prend la valeur existante pour cette clé, plus la valeur que j'essaie d'ajouter . Si la clé n'existe pas encore dans la carte, ajoutez-la et sa valeur reste inchangée.
Dans mon cas spécifique, j'aimerais construire une seule carte [String, Double] de telle sorte que si la carte contient déjà une clé, le double sera ajouté à la valeur de la carte existante.
Je travaille avec des cartes mutables dans mon code spécifique, mais je suis intéressé par des solutions plus génériques, si possible.
Celui-ci, ça va:
def mergeMap[A, B](ms: List[Map[A, B]])(f: (B, B) => B): Map[A, B] =
(Map[A, B]() /: (for (m <- ms; kv <- m) yield kv)) { (a, kv) =>
a + (if (a.contains(kv._1)) kv._1 -> f(a(kv._1), kv._2) else kv)
}
val ms = List(Map("hello" -> 1.1, "world" -> 2.2), Map("goodbye" -> 3.3, "hello" -> 4.4))
val mm = mergeMap(ms)((v1, v2) => v1 + v2)
println(mm) // prints Map(hello -> 5.5, world -> 2.2, goodbye -> 3.3)
Et cela fonctionne dans les versions 2.7.5 et 2.8.0.
Eh bien, vous pourriez faire:
mapList reduce (_ ++ _)
sauf pour l'exigence spéciale de collision.
Puisque vous avez cette exigence spéciale, le mieux serait peut-être de faire quelque chose comme ça (2.8):
def combine(m1: Map, m2: Map): Map = {
val k1 = Set(m1.keysIterator.toList: _*)
val k2 = Set(m2.keysIterator.toList: _*)
val intersection = k1 & k2
val r1 = for(key <- intersection) yield (key -> (m1(key) + m2(key)))
val r2 = m1.filterKeys(!intersection.contains(_)) ++ m2.filterKeys(!intersection.contains(_))
r2 ++ r1
}
Vous pouvez ensuite ajouter cette méthode à la classe de carte via le modèle Pimp My Library et l'utiliser dans l'exemple d'origine au lieu de "++
":
class CombiningMap(m1: Map[Symbol, Double]) {
def combine(m2: Map[Symbol, Double]) = {
val k1 = Set(m1.keysIterator.toList: _*)
val k2 = Set(m2.keysIterator.toList: _*)
val intersection = k1 & k2
val r1 = for(key <- intersection) yield (key -> (m1(key) + m2(key)))
val r2 = m1.filterKeys(!intersection.contains(_)) ++ m2.filterKeys(!intersection.contains(_))
r2 ++ r1
}
}
// Then use this:
implicit def toCombining(m: Map[Symbol, Double]) = new CombiningMap(m)
// And finish with:
mapList reduce (_ combine _)
Alors que ceci a été écrit en 2.8, keysIterator
devient keys
pour 2.7, filterKeys
devra peut-être être écrit en termes de filter
et map
, &
deviendra **
, etc., il ne devrait pas être trop différent.
Je suis surpris que personne ne propose encore cette solution:
myListOfMaps.flatten.toMap
Fait exactement ce dont vous avez besoin:
Exemple:
scala> List(Map('a -> 1), Map('b -> 2), Map('c -> 3), Map('a -> 4, 'b -> 5)).flatten.toMap
res7: scala.collection.immutable.Map[Symbol,Int] = Map('a -> 4, 'b -> 5, 'c -> 3)
flatten
transforme la liste des cartes en une liste plate de tuples, toMap
transforme la liste des tuples en une carte avec toutes les clés en double supprimées
En lisant cette question rapidement, je ne suis pas sûr qu'il me manque quelque chose (comme cela doit fonctionner pour 2.7.x ou pas de scalaz):
import scalaz._
import Scalaz._
val ms = List(Map("hello" -> 1.1, "world" -> 2.2), Map("goodbye" -> 3.3, "hello" -> 4.4))
ms.reduceLeft(_ |+| _)
// returns Map(goodbye -> 3.3, hello -> 5.5, world -> 2.2)
Vous pouvez changer la définition du monoïde pour Double et obtenir un autre moyen d’accumuler les valeurs, ici le max:
implicit val dbsg: Semigroup[Double] = semigroup((a,b) => math.max(a,b))
ms.reduceLeft(_ |+| _)
// returns Map(goodbye -> 3.3, hello -> 4.4, world -> 2.2)
Intéressant, nouant un peu avec ceci, j'ai eu le suivant (sur 2.7.5):
Cartes générales:
def mergeMaps[A,B](collisionFunc: (B,B) => B)(listOfMaps: Seq[scala.collection.Map[A,B]]): Map[A, B] = {
listOfMaps.foldLeft(Map[A, B]()) { (m, s) =>
Map(
s.projection.map { pair =>
if (m contains pair._1)
(pair._1, collisionFunc(m(pair._1), pair._2))
else
pair
}.force.toList:_*)
}
}
Mais l'homme, c'est hideux avec la projection et le forçage, la liste et tout le reste. Question distincte: quel meilleur moyen de régler ce problème au sein du groupe?
Pour les cartes mutables, ce que je traitais dans mon code, et avec une solution moins générale, j'ai eu ceci:
def mergeMaps[A,B](collisionFunc: (B,B) => B)(listOfMaps: List[mutable.Map[A,B]]): mutable.Map[A, B] = {
listOfMaps.foldLeft(mutable.Map[A,B]()) {
(m, s) =>
for (k <- s.keys) {
if (m contains k)
m(k) = collisionFunc(m(k), s(k))
else
m(k) = s(k)
}
m
}
}
Cela semble un peu plus clair, mais ne fonctionnera qu'avec les cartes mutables telles qu'elles sont écrites. Fait intéressant, j'ai d'abord essayé ce qui précède (avant de poser la question) en utilisant /: au lieu de foldLeft, mais des erreurs de frappe se sont produites. Je pensais que /: et foldLeft étaient fondamentalement équivalents, mais le compilateur n'arrêtait pas de se plaindre d'avoir besoin de types explicites pour (m, s). Quoi de neuf avec ça?
J'ai écrit un billet de blog à ce sujet, allez voir ça:
http://www.nimrodstech.com/scala-map-merge/
essentiellement en utilisant le groupe semi scalaz, vous pouvez y arriver assez facilement
ressemblerait à quelque chose comme:
import scalaz.Scalaz._
listOfMaps reduce(_ |+| _)
Démarrer Scala 2.13
, une autre solution qui gère les clés en double et n’est que basé sur la bibliothèque standard consiste à fusionner les Map
s en tant que séquences (flatten
) avant d’appliquer le nouvel opérateur groupMapReduce qui (comme son nom l'indique) est l'équivalent d'une groupBy
suivie d'un mappage et d'une étape de réduction des valeurs groupées:
List(Map("hello" -> 1.1, "world" -> 2.2), Map("goodbye" -> 3.3, "hello" -> 4.4))
.flatten
.groupMapReduce(_._1)(_._2)(_ + _)
// Map("world" -> 2.2, "goodbye" -> 3.3, "hello" -> 5.5)
Ce:
flatten
s (concatène) les cartes en une séquence de nuplets (List(("hello", 1.1), ("world", 2.2), ("goodbye", 3.3), ("hello", 4.4))
), qui conserve toutes les clés/valeurs (même les clés dupliquées)
group
s éléments basés sur leur première partie Tuple (_._1
) (partie groupe de groupe MapReduce)
map
s a groupé les valeurs dans leur deuxième partie Tuple (_._2
) (partie de la carte du groupe Map Réduire)
reduce
s mappées valeurs groupées (_+_
) en prenant leur somme (mais il peut s'agir de n'importe quelle fonction reduce: (T, T) => T
) (réduction d'une partie de groupMap réduction )
L'étape groupMapReduce
peut être vue comme une version à une passe équivalente à:
list.groupBy(_._1).mapValues(_.map(_._2).reduce(_ + _))
oneliner helper-func, dont l'utilisation est presque aussi propre que celle de scalaz:
def mergeMaps[K,V](m1: Map[K,V], m2: Map[K,V])(f: (V,V) => V): Map[K,V] =
(m1 -- m2.keySet) ++ (m2 -- m1.keySet) ++ (for (k <- m1.keySet & m2.keySet) yield { k -> f(m1(k), m2(k)) })
val ms = List(Map("hello" -> 1.1, "world" -> 2.2), Map("goodbye" -> 3.3, "hello" -> 4.4))
ms.reduceLeft(mergeMaps(_,_)(_ + _))
// returns Map(goodbye -> 3.3, hello -> 5.5, world -> 2.2)
pour une lisibilité ultime, enveloppez-le dans un type personnalisé implicite:
class MyMap[K,V](m1: Map[K,V]) {
def merge(m2: Map[K,V])(f: (V,V) => V) =
(m1 -- m2.keySet) ++ (m2 -- m1.keySet) ++ (for (k <- m1.keySet & m2.keySet) yield { k -> f(m1(k), m2(k)) })
}
implicit def toMyMap[K,V](m: Map[K,V]) = new MyMap(m)
val ms = List(Map("hello" -> 1.1, "world" -> 2.2), Map("goodbye" -> 3.3, "hello" -> 4.4))
ms reduceLeft { _.merge(_)(_ + _) }