Скала:как объединить коллекцию карт
-
13-09-2019 - |
Вопрос
У меня есть список Map[String, Double], и я хотел бы объединить их содержимое в одну Map [String, Double].Как я должен сделать это идиоматическим способом?Я полагаю, что я должен быть в состоянии сделать это со сгибом.Что -то вроде:
val newMap = Map[String, Double]() /: listOfMaps { (accumulator, m) => ... }
Кроме того, я бы хотел обрабатывать столкновения ключей общим способом.То есть, если я добавлю ключ к карте, который уже существует, я должен быть в состоянии указать функцию, которая возвращает Double (в данном случае) и принимает существующее значение для этого ключа, плюс значение, которое я пытаюсь добавить.Если ключ еще не существует на карте, то просто добавьте его и его значение без изменений.
В моем конкретном случае я хотел бы создать единую карту [String, Double] таким образом, что если карта уже содержит ключ, то Double будет добавлен к существующему значению карты.
Я работаю с изменяемыми картами в моем конкретном коде, но меня интересуют более общие решения, если это возможно.
Решение
Как насчет этого:
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)
И это работает как в 2.7.5, так и в 2.8.0.
Другие советы
Что ж, вы могли бы сделать:
mapList reduce (_ ++ _)
за исключением особых требований при столкновении.
Поскольку у вас действительно есть это особое требование, возможно, лучше всего было бы сделать что-то вроде этого (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
}
Затем вы можете добавить этот метод в класс map с помощью шаблона Pimp My Library и использовать его в исходном примере вместо "++
":
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 _)
Хотя это было написано в 2.8, так что keysIterator
становится keys
для 2.7, filterKeys
возможно, потребуется написать в терминах filter
и map
, &
становится **
, и так далее, это не должно слишком отличаться.
Я удивлен, что никто до сих пор не придумал это решение:
myListOfMaps.flatten.toMap
Делает именно то, что вам нужно:
- Объединяет список в единую карту
- Удаляет все дубликаты ключей
Пример:
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
превращает список карт в плоский список кортежей, toMap
превращает список кортежей в карту с удалением всех дублирующихся ключей
Я быстро прочитал этот вопрос, поэтому я не уверен, что я что-то упускаю (например, это должно работать для 2.7.x или без 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)
Вы можете изменить определение monoid на Double и получить другой способ накопления значений, здесь получая максимальное:
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)
Интересно, немного повозившись с этим, я получил следующее (на 2.7.5):
Общие Карты:
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:_*)
}
}
Но, черт возьми, это отвратительно - проекция, принуждение, толист и все такое прочее.Отдельный вопрос:какой лучший способ справиться с этим внутри группы?
Для изменяемых карт, с которыми я имел дело в своем коде, и с менее общим решением я получил следующее:
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
}
}
Это кажется немного более чистым, но будет работать только с изменяемыми картами в том виде, в каком они написаны.Интересно, что я впервые попробовал вышеописанное (до того, как задал вопрос), используя /:вместо foldLeft, но я получал ошибки типа.Я думал /:и foldLeft были в основном эквивалентны, но компилятор продолжал жаловаться, что мне нужны явные типы для (m, s).Что с этим не так?
Я написал об этом сообщение в блоге , ознакомьтесь с ним :
http://www.nimrodstech.com/scala-map-merge/
в принципе, используя полугруппу scalaz, вы можете добиться этого довольно легко
выглядело бы что-то вроде :
import scalaz.Scalaz._
listOfMaps reduce(_ |+| _)
Начиная Scala 2.13
, другое решение , которое обрабатывает дубликаты ключей и это всего лишь основано на стандартной библиотеке заключается в объединении Map
s как последовательности (flatten
) перед нанесением нового Групповое создание оператор, который (как следует из его названия) является эквивалентом groupBy
за которым следует сопоставление и шаг уменьшения сгруппированных значений:
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)
Это:
flatten
s (объединяет) карты в виде последовательности кортежей (List(("hello", 1.1), ("world", 2.2), ("goodbye", 3.3), ("hello", 4.4))
), который сохраняет все ключи / значения (даже дублирующиеся ключи)group
элементы s , основанные на их первой части кортежа (_._1
) (групповая часть ГруппаMapReduce)map
s сгруппировали значения в их вторую часть кортежа (_._2
) (карта части группыКартаУменьшить)reduce
сопоставленные сгруппированные значения (_+_
), взяв их сумму (но это может быть любаяreduce: (T, T) => T
функция) (уменьшить часть GroupMapУменьшить)
Тот Самый groupMapReduce
шаг можно рассматривать как однопроходная версия эквивалент:
list.groupBy(_._1).mapValues(_.map(_._2).reduce(_ + _))
вспомогательная функция oneliner, использование которой читается почти так же чисто, как использование 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)
для максимальной удобочитаемости оберните его в неявный пользовательский тип:
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(_)(_ + _) }