Scala: объединить карты по ключу

Скажем, у меня есть две карты:

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

Я хочу объединить эти карты по ключу, применяя некоторую функцию для сбора значений (в данном конкретном случае я хочу собрать их в последовательность, давая:

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

Такое чувство, что должен быть хороший идиоматичный способ сделать это - какие-нибудь предложения? Я рад, если решение включает скаляз.

6 ответов

Решение

scala.collection.immutable.IntMap имеет intersectionWith метод, который делает именно то, что вы хотите (я считаю):

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

Это дает вам IntMap(1 -> List(one, un), 2 -> List(two, deux), 3 -> List(three, trois)), Обратите внимание, что он правильно игнорирует ключ, который встречается только в a,

Как примечание: я часто ловил себя на желании unionWith, intersectionWith и др. функции от Хаскелла Data.Map в Скале. Я не думаю, что есть какая-либо принципиальная причина, что они должны быть доступны только на IntMap а не в базе collection.Map черта характера.

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

Скалаз добавляет метод |+| для любого типа A для чего Semigroup[A] доступен.

Если вы сопоставили свои Карты так, чтобы каждое значение было последовательностью из одного элемента, то вы могли бы использовать это довольно просто:

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

Запуск Scala 2.13, вы можете использовать groupMap который (как следует из названия) является эквивалентом groupBy с последующим map по значениям:

// val map1 = Map(1 -> "one", 2 -> "two",  3 -> "three")
// val map2 = Map(1 -> "un",  2 -> "deux", 3 -> "trois")
(map1.toSeq ++ map2).groupMap(_._1)(_._2)
// Map(1 -> List("one", "un"), 2 -> List("two", "deux"), 3 -> List("three", "trois"))

Эта:

  • Объединяет две карты как последовательность кортежей (List((1, "one"), (2, "two"), (3, "three"))). Для краткости,map2это неявно преобразуется вSeq согласовывать с map1.toSeqтип - но вы можете сделать его явным, используя map2.toSeq.

  • groups элементов на основе их первой части кортежа (_._1) (групповая часть групповой карты)

  • maps сгруппировали значения во вторую часть кортежа (_._2) (часть карты группы Map)

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)]

Поэтому я не был доволен ни одним из этих решений (я хочу создать новый тип, так что полугруппа на самом деле не чувствует себя уместно, а решение Infinity казалось довольно сложным), поэтому я согласился с этим на данный момент. Я был бы рад видеть, что это улучшилось:

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(_,_)}

Я хотел, чтобы поведение ничего не возвращало, когда ключ не присутствовал ни на одной карте (что отличается от других решений), но способ указать это был бы неплох.

Вот мой первый подход, прежде чем искать другие решения:

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

Чтобы избежать элементов, которые существуют только в a или b, фильтр удобен:

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

Flatten необходим, потому что b.get (x._1) возвращает Option. Чтобы сгладить работу, первый элемент также должен быть опцией, поэтому мы не можем просто использовать x._2 здесь.

Для последовательностей это тоже работает:

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)))
def merge[A,B,C,D](b : Map[A,B], c : Map[A,C])(d : (Option[B],Option[C]) => D): Map[A,D] = {
  (b.keySet ++ c.keySet).map(k => k -> d(b.get(k), c.get(k))).toMap
}

def optionSeqBiFunctionK[A]:(Option[A], Option[A]) => Seq[A] = _.toSeq ++ _.toSeq

merge(a,b)(optionSeqBiFunctionK)

Другие вопросы по тегам