Scala: Combinar mapas por clave


Digamos que tengo dos mapas:

val a = Map(1 -> "one", 2 -> "two", 3 -> "three")
val b = Map(1 -> "un", 2 -> "deux", 3 -> "trois")

Quiero combinar estos mapas por clave, aplicando alguna función para recopilar los valores (en este caso particular quiero recopilarlos en un seq, dando:

val c = Map(1 -> Seq("one", "un"), 2->Seq("two", "deux"), 3->Seq("three", "trois"))

Parece que debería haber una forma agradable e idiomática de hacer esto - ¿alguna sugerencia? Estoy feliz si la solución implica scalaz.

Author: Submonoid, 2011-10-13

6 answers

scala.collection.immutable.IntMap tiene un método intersectionWith que hace exactamente lo que quieres (creo):

import scala.collection.immutable.IntMap

val a = IntMap(1 -> "one", 2 -> "two", 3 -> "three", 4 -> "four")
val b = IntMap(1 -> "un", 2 -> "deux", 3 -> "trois")

val merged = a.intersectionWith(b, (_, av, bv: String) => Seq(av, bv))

Esto te da IntMap(1 -> List(one, un), 2 -> List(two, deux), 3 -> List(three, trois)). Tenga en cuenta que ignora correctamente la clave que solo aparece en a.

Como nota al margen: A menudo me he encontrado queriendo el unionWith, intersectionWith, etc. funciones de Haskell's Data.Map en Scala. No creo que haya ninguna razón de principio para que solo estén disponibles en IntMap, en lugar de en el rasgo base collection.Map.

 19
Author: Travis Brown,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-13 18:03:31
val a = Map(1 -> "one", 2 -> "two", 3 -> "three")
val b = Map(1 -> "un", 2 -> "deux", 3 -> "trois")

val c = a.toList ++ b.toList
val d = c.groupBy(_._1).map{case(k, v) => k -> v.map(_._2).toSeq}
//res0: scala.collection.immutable.Map[Int,Seq[java.lang.String]] =
        //Map((2,List(two, deux)), (1,List(one, un), (3,List(three, trois)))
 18
Author: Infinity,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-13 14:10:00

Scalaz añade un método |+| para cualquier tipo A para el que esté disponible un Semigroup[A].

Si asigna sus mapas de modo que cada valor sea una secuencia de un solo elemento, entonces podría usar esto simplemente:

scala> a.mapValues(Seq(_)) |+| b.mapValues(Seq(_))
res3: scala.collection.immutable.Map[Int,Seq[java.lang.String]] = Map(1 -> List(one, un), 2 -> List(two, deux), 3 -> List(three, trois))
 14
Author: Ben James,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-13 14:11:30

Así que no estaba muy contento con ninguna solución (quiero construir un nuevo tipo, por lo que semigroup no se siente realmente apropiado, y la solución de Infinity parecía bastante compleja), así que he ido con esto por el momento. Me encantaría verlo mejorado:

def merge[A,B,C](a : Map[A,B], b : Map[A,B])(c : (B,B) => C) = {
  for (
    key <- (a.keySet ++ b.keySet);
    aval <- a.get(key); bval <- b.get(key)
  ) yield c(aval, bval)
}
merge(a,b){Seq(_,_)}

Quería el comportamiento de no devolver nada cuando una clave no estaba presente en ninguno de los mapas (lo que difiere de otras soluciones), pero una forma de especificar esto sería buena.

 2
Author: Submonoid,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-13 14:40:43

Aquí está mi primer enfoque antes de buscar las otras soluciones:

for (x <- a) yield 
  x._1 -> Seq (a.get (x._1), b.get (x._1)).flatten

Para evitar elementos que solo existen en a o b, un filtro es útil:

(for (x <- a) yield 
  x._1 -> Seq (a.get (x._1), b.get (x._1)).flatten).filter (_._2.size == 2)

Aplanar es necesario, porque b.get (x._1) devuelve una opción. Para hacer que el trabajo de aplanar, el primer elemento tiene que ser una opción también, por lo que no podemos usar x._2 aquí.

Para secuencias, también funciona:

scala> val b = Map (1 -> Seq(1, 11, 111), 2 -> Seq(2, 22), 3 -> Seq(33, 333), 5 -> Seq(55, 5, 5555))
b: scala.collection.immutable.Map[Int,Seq[Int]] = Map(1 -> List(1, 11, 111), 2 -> List(2, 22), 3 -> List(33, 333), 5 -> List(55, 5, 5555))

scala> val a = Map (1 -> Seq(1, 101), 2 -> Seq(2, 212, 222), 3 -> Seq (3, 3443), 4 -> (44, 4, 41214))
a: scala.collection.immutable.Map[Int,ScalaObject with Equals] = Map(1 -> List(1, 101), 2 -> List(2, 212, 222), 3 -> List(3, 3443), 4 -> (44,4,41214))

scala> (for (x <- a) yield x._1 -> Seq (a.get (x._1), b.get (x._1)).flatten).filter (_._2.size == 2) 
res85: scala.collection.immutable.Map[Int,Seq[ScalaObject with Equals]] = Map(1 -> List(List(1, 101), List(1, 11, 111)), 2 -> List(List(2, 212, 222), List(2, 22)), 3 -> List(List(3, 3443), List(33, 333)))
 1
Author: user unknown,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-14 10:19:38
val fr = Map(1 -> "one", 2 -> "two", 3 -> "three")
val en = Map(1 -> "un", 2 -> "deux", 3 -> "trois")

def innerJoin[K, A, B](m1: Map[K, A], m2: Map[K, B]): Map[K, (A, B)] = {
  m1.flatMap{ case (k, a) => 
    m2.get(k).map(b => Map((k, (a, b)))).getOrElse(Map.empty[K, (A, B)])
  }
}

innerJoin(fr, en) // Map(1 -> ("one", "un"), 2 -> ("two", "deux"), 3 -> ("three", "trois")): Map[Int, (String, String)]
 1
Author: Guillaume Massé,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-01 18:20:01