2009-08-11 15 views
28

Mam listę map [String, Double] i chciałbym scalić ich zawartość w jedną mapę [String, Double]. Jak mam to robić w sposób idiomatyczny? Wyobrażam sobie, że powinienem był to zrobić z zakładką. Coś jak:Scala: jak połączyć kolekcję map

val newMap = Map[String, Double]() /: listOfMaps { (accumulator, m) => ... } 

Ponadto, chciałbym obsługiwać kolizje kluczy w sposób ogólny. Oznacza to, że jeśli dodaję klucz do mapy, która już istnieje, powinienem móc określić funkcję, która zwraca Double (w tym przypadku) i pobiera istniejącą wartość dla tego klucza, a także wartość, którą próbuję dodać . Jeśli klucz jeszcze nie istnieje na mapie, po prostu dodaj go, a jego wartość niezmienioną.

W moim konkretnym przypadku chciałbym zbudować pojedynczą mapę [String, Double] tak, że jeśli mapa zawiera już klucz, to Double zostanie dodane do istniejącej wartości mapy.

Pracuję z mapami zmiennymi w moim specyficznym kodzie, ale jestem zainteresowany bardziej ogólnymi rozwiązaniami, jeśli to możliwe.

Odpowiedz

23

Jak o tym jednym:

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) 

I to działa zarówno w 2.7.5 i 2.8.0.

+0

To jest dokładnie to, jak próbowałem to zrobić na początku. Nie myślałem o tym, żebym to zrozumiał - wciąż się do tego przyzwyczajam, ale to ma sens. W tym przypadku widzę, jak bardzo przypomina to rozumienie list Pythona, z którym jestem o wiele wygodniejszy. Podobnie jak użycie wyniku, jeśli wyrażenie wewnątrz wywołania a. +(). – Jeff

+0

czysta odpowiedź. kudos –

37

Cóż, można zrobić:

mapList reduce (_ ++ _) 

wyjątkiem szczególnego wymogu kolizji.

Skoro masz ten specjalny warunek, może najlepiej byłoby zrobić coś takiego (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 
} 

Następnie można dodać tę metodę do klasy mapy poprzez wzór Pimp My Library, a używać go w oryginalnym przykład zamiast „++”:

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

Chociaż ten został napisany w 2,8, tak keysIterator staje keys do 2,7 filterKeys może trzeba być napisane w kategoriach filter i map, & staje się **, i tak dalej, nie powinno to być zbyt różne.

+1

Kinda pokonuje punkt ignorowanie tego wymogu. – Jeff

+0

Dlatego właśnie go rozszerzyłem. –

+0

Z nowoczesną wersją Scala: val k1 = m1.keysIterator.toSet – qerub

2

Ciekawe noodling wokół z tego kawałka, mam następujące (na 2.7.5):

ogólne Mapy:

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:_*) 
    } 
    } 

Ale człowiek, który jest ohydny z projekcją i wymuszanie i toList i co tam. Oddzielne pytanie: jaki jest lepszy sposób radzenia sobie z tym w zakładce?

Dla modyfikowalnych Maps, czyli to, co mam do czynienia z moim kodzie, a także z mniej ogólne rozwiązanie, mam to:

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

To wydaje się trochę czystsze, ale działa tylko z mutable Mapy, jak jest napisane. Co ciekawe, najpierw wypróbowałem powyższe (zanim zadałem pytanie) używając /: zamiast foldLeft, ale otrzymywałem błędy typu. Myślałem /: i foldLeft były w zasadzie równoważne, ale kompilator ciągle narzekał, że potrzebuję jawnych typów dla (m, s). Co z tym?

+0

Nie musisz używać 'force' tutaj, ponieważ' toList' jest ścisłe. –

+0

Co do 'foldLeft' kontra' /: ', zdajesz sobie sprawę, że obiekt i pierwszy argument są zamieniane między nimi? Wyrażenie 'x foldLeft y' jest równoważne' y /: x'. Poza tym istnieje kilka problemów z składnią. Zasadniczo * masz *, aby napisać '(y /: x) (wyrażenie składane)', natomiast 'foldLeft' może być użyte jako' x.foldLeft (y) (wyrażenie składane) '. –

+0

Tak, wiedziałem o metodach kończących się na: zamianę obiektu na argument. Tak napisałem przykład w pytaniu. Zapomniałem jednak wstawić y /: x w parens i założę się, że to był problem. Dzięki! – Jeff

3

ja czytając to pytanie szybko, więc nie jestem pewien, czy ja czegoś brakuje (jak to ma działać na 2.7.x lub bez 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) 

Można zmienić definicję monoid dla podwójne i dostać inny sposób gromadzić wartości, tu coraz 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) 
+0

+1, chociaż napisałbym 'ms.suml', który jest bardziej zwięzły i ma dodatkową zaletę, że nie rzuca wyjątku czasu wykonywania na pustą listę. –

+0

@TravisBrown, tak, tak wiele wygodnych funkcji w skalaz; chociaż 'suml' może być tylko skalazem 7? Widzę tylko 'sumr' w 6.x. – huynhjl

0

się oneliner pomocnika-func, którego wykorzystanie brzmi prawie tak czysty, jak przy użyciu 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) 

na ostateczny czytelność zawinąć go w sposób dorozumiany typu niestandardowe:

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

napisałem na blogu na ten temat, to sprawdzić:

http://www.nimrodstech.com/scala-map-merge/

zasadzie korzystania scalaz pół grupa może osiągnąć to całkiem łatwo będzie wyglądać tak:

będzie wyglądać jak:

import scalaz.Scalaz._ 
    listOfMaps reduce(_ |+| _) 
+0

Możesz faktycznie użyć 'listOfMaps.suml'; powinien zrobić to samo. z tego co rozumiem oznacza sumLeft, gdzie zasadniczo działa 'reduceLeft (_ | + | _)' – JBarber

17

Dziwię nikt pochodzić z tego rozwiązania jeszcze:

myListOfMaps.flatten.toMap 

Robi dokładnie to, czego potrzebujesz:

  1. Scala listę do jednej mapie
  2. Weeds jakichkolwiek duplikat klucze

Przykład:

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 włącza listę map do płaskiej listy krotek, toMap okazuje listę krotek na mapie za pomocą wszystkich duplikatów kluczy usunięte

+2

To jest dokładnie to, czego potrzebowałem, ale nie sumuje wartości dla duplikatów kluczy, jak wymaga tego OP. –

+0

Lub możesz użyć flatMap – wbmrcb