web-dev-qa-db-fra.com

Scala: comment fusionner une collection de cartes

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.

32
Jeff

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.

26
Walter Chang

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.

39
Daniel C. Sobral

Je suis surpris que personne ne propose encore cette solution:

myListOfMaps.flatten.toMap

Fait exactement ce dont vous avez besoin:

  1. Fusionne la liste en une seule carte
  2. Élimine les clés en double

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

21
Electric Coffee

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)
3
huynhjl

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?

2
Jeff

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(_ |+| _)
2
Nimrod007

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 Maps 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:

  • flattens (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)

  • groups éléments basés sur leur première partie Tuple (_._1) (partie groupe de groupe MapReduce)

  • maps a groupé les valeurs dans leur deuxième partie Tuple (_._2) (partie de la carte du groupe Map Réduire)

  • reduces 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(_ + _))
0
Xavier Guihot

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(_)(_ + _) } 
0
bernstein