Scala beste Möglichkeit, eine Sammlung in eine Map-by-Key zu verwandeln?

165

Wenn ich eine Sammlung cvon Typen habe Tund eine Eigenschaft pfür T(z P. B. Typ ) vorhanden ist, wie kann ich einen Map-by-Extracting-Key am besten erstellen ?

val c: Collection[T]
val m: Map[P, T]

Ein Weg ist der folgende:

m = new HashMap[P, T]
c foreach { t => m add (t.getP, t) }

Aber jetzt brauche ich eine veränderbare Karte. Gibt es eine bessere Möglichkeit, dies so zu tun, dass es in einer Zeile steht und ich eine unveränderliche Karte erhalte ? (Natürlich könnte ich das oben genannte in ein einfaches Bibliotheksdienstprogramm verwandeln, wie ich es in Java tun würde, aber ich vermute, dass es in Scala keine Notwendigkeit gibt)

oxbow_lakes
quelle

Antworten:

231

Sie können verwenden

c map (t => t.getP -> t) toMap

Beachten Sie jedoch, dass dies 2 Durchquerungen erfordert.

Ben Lings
quelle
8
Ich bevorzuge immer noch meine Vorschläge in Trac a Traversable[K].mapTo( K => V)und Traversable[V].mapBy( V => K)waren besser!
oxbow_lakes
7
Beachten Sie, dass dies eine quadratische Operation ist, dies gilt jedoch auch für die meisten anderen hier angegebenen Varianten. Wenn ich mir den Quellcode von scala.collection.mutable.MapBuilder usw. anschaue, scheint es mir, dass für jedes Tupel eine neue unveränderliche Map erstellt wird, zu der das Tupel hinzugefügt wird.
jcsahnwaldt Reinstate Monica
30
Auf meinem Computer für eine Liste mit 500.000 Elementen ist dieser Scala-Code etwa 20-mal langsamer als der einfache Java-Ansatz (HashMap mit der entsprechenden Größe erstellen, Liste durchlaufen, Elemente in die Karte einfügen). Bei 5.000 Elementen ist Scala etwa achtmal langsamer. Der in Scala geschriebene Loop-Ansatz ist ungefähr dreimal schneller als die toMap-Variante, aber immer noch zwei- bis siebenmal langsamer als Java.
jcsahnwaldt Reinstate Monica
8
Würden Sie bitte die Testquellen der SO-Community zur Verfügung stellen? Vielen Dank.
user573215
8
Ersetzen cdurch c.iterator, um die Erstellung einer Zwischensammlung zu vermeiden.
Ghik
21

Sie können eine Karte mit einer variablen Anzahl von Tupeln erstellen. Verwenden Sie also die Map-Methode für die Sammlung, um sie in eine Sammlung von Tupeln zu konvertieren, und verwenden Sie dann den Trick: _ *, um das Ergebnis in ein variables Argument zu konvertieren.

scala> val list = List("this", "maps", "string", "to", "length") map {s => (s, s.length)}
list: List[(java.lang.String, Int)] = List((this,4), (maps,4), (string,6), (to,2), (length,6))

scala> val list = List("this", "is", "a", "bunch", "of", "strings")
list: List[java.lang.String] = List(this, is, a, bunch, of, strings)

scala> val string2Length = Map(list map {s => (s, s.length)} : _*)
string2Length: scala.collection.immutable.Map[java.lang.String,Int] = Map(strings -> 7, of -> 2, bunch -> 5, a -> 1, is -> 2, this -> 4)
James Iry
quelle
5
Ich habe über 2 Wochen über Scala gelesen und Beispiele durchgearbeitet und nicht ein einziges Mal diese ": _ *" - Notation gesehen! Vielen Dank für Ihre Hilfe
oxbow_lakes
Nur zur Veranschaulichung, ich frage mich, warum wir präzisieren müssen, dass dies eine Sequenz mit _ ist . map noch konvertieren hier eine Liste von Tupeln zurückgeben. Warum also das _ ? Ich meine, es funktioniert, aber ich möchte die
Typzuweisung
1
Ist das effizienter als die anderen Methoden?
Jus12
16

Zusätzlich zur Lösung von @James Iry ist dies auch mit einer Falte möglich. Ich vermute, dass diese Lösung etwas schneller ist als die Tupelmethode (es werden weniger Müllobjekte erstellt):

val list = List("this", "maps", "string", "to", "length")
val map = list.foldLeft(Map[String, Int]()) { (m, s) => m(s) = s.length }
Daniel Spiewak
quelle
Ich werde das ausprobieren (ich bin sicher, es funktioniert :-). Was ist los mit der Funktion "(m, s) => m (s) = s.length"? Ich habe das typische foldLeft-Beispiel mit einer Summe und einer Funktion "_ + _" gesehen. das ist viel verwirrender! Die Funktion scheint anzunehmen, dass ich bereits ein Tupel (m, s) habe, das ich nicht wirklich bekomme
oxbow_lakes
2
Mann, Scala war damals komisch!
fehlender Faktor
8
@ Daniel Ich versuche Ihren Code, erhalte aber folgenden Fehler: "Wertaktualisierung ist kein Mitglied von scala.collection.immutable.Map [String, Int]". Bitte erläutern Sie Ihren Code, wie dieser Code funktioniert.
Mr. Boyfox
1
scheint nicht zu funktionieren. Für mich auch "Anwendung nimmt keine Parameter"
jayunit100
7
Unveränderliche Version : list.foldLeft(Map[String,Int]()) { (m,s) => m + (s -> s.length) }. Beachten Sie, dass Sie ein zusätzliches Klammerpaar benötigen, wenn Sie das Tupel mit Komma erstellen möchten : ((s, s.length)).
Kelvin
11

Dies kann unveränderlich und mit einer einzigen Durchquerung implementiert werden, indem die Sammlung wie folgt gefaltet wird.

val map = c.foldLeft(Map[P, T]()) { (m, t) => m + (t.getP -> t) }

Die Lösung funktioniert, weil das Hinzufügen zu einer unveränderlichen Karte eine neue unveränderliche Karte mit dem zusätzlichen Eintrag zurückgibt und dieser Wert während der Faltoperation als Akkumulator dient.

Der Kompromiss hier ist die Einfachheit des Codes gegenüber seiner Effizienz. Für große Sammlungen ist dieser Ansatz möglicherweise besser geeignet als die Verwendung von zwei Durchquerungsimplementierungen, z. B. Anwenden von mapund toMap.

RamV13
quelle
8

Eine andere Lösung (funktioniert möglicherweise nicht für alle Typen)

import scala.collection.breakOut
val m:Map[P, T] = c.map(t => (t.getP, t))(breakOut)

Dadurch wird die Erstellung der Zwischenliste vermieden. Weitere Informationen finden Sie hier: Scala 2.8 breakOut

Somatik
quelle
7

Was Sie erreichen wollen, ist etwas undefiniert.
Was ist, wenn zwei oder mehr Elemente cdasselbe teilen p? Welches Element wird dem pin der Karte zugeordnet?

Die genauere Sichtweise besteht darin, eine Karte zwischen pund allen cElementen zu erstellen, die sie haben:

val m: Map[P, Collection[T]]

Dies könnte leicht mit groupBy erreicht werden :

val m: Map[P, Collection[T]] = c.groupBy(t => t.p)

Wenn Sie die ursprüngliche Karte weiterhin möchten, können Sie beispielsweise pdie erste Karte zuordnen , die tsie enthält:

val m: Map[P, T] = c.groupBy(t => t.p) map { case (p, ts) =>  p -> ts.head }
Eyal Roth
quelle
1
Eine praktische Verbesserung ist die Verwendung von collectanstelle von map. ZB : c.group(t => t.p) collect { case (Some(p), ts) => p -> ts.head }. Auf diese Weise können Sie beispielsweise Karten reduzieren, wenn Sie eine Option [_] eingeben.
Healsjnr
@healsjnr Sicher, das könnte man für jede Karte sagen. Es ist hier jedoch nicht das Kernthema.
Eyal Roth
1
Sie können .mapValues(_.head)anstelle der Karte verwenden.
Lex82
2

Dies ist wahrscheinlich nicht die effizienteste Methode, um eine Liste in eine Karte umzuwandeln, macht den aufrufenden Code jedoch besser lesbar. Ich habe implizite Konvertierungen verwendet, um List eine mapBy- Methode hinzuzufügen :

implicit def list2ListWithMapBy[T](list: List[T]): ListWithMapBy[T] = {
  new ListWithMapBy(list)
}

class ListWithMapBy[V](list: List[V]){
  def mapBy[K](keyFunc: V => K) = {
    list.map(a => keyFunc(a) -> a).toMap
  }
}

Beispiel für einen Aufrufcode:

val list = List("A", "AA", "AAA")
list.mapBy(_.length)                  //Map(1 -> A, 2 -> AA, 3 -> AAA)

Beachten Sie, dass der Aufrufercode aufgrund der impliziten Konvertierung die implicitConversions von scala importieren muss.

Erez
quelle
2
c map (_.getP) zip c

Funktioniert gut und ist sehr intuitiv

Jörg Bächtiger
quelle
8
Bitte fügen Sie weitere Details hinzu.
Syeda Zunaira
2
Es tut mir Leid. Dies ist jedoch eine Antwort auf die Frage "Scala ist der beste Weg, eine Sammlung in eine Map-by-Key zu verwandeln?" wie Ben Lings ist.
Jörg Bächtiger
1
Und Ben hat keine Erklärung geliefert?
Shinzou
1
Dadurch werden zwei Listen erstellt und unter Verwendung der Elemente cals Schlüssel (Art) zu einer "Karte" kombiniert . Beachten Sie "map", da die resultierende Sammlung keine Scala ist, Mapsondern eine andere Liste / Iterable von Tupeln erstellt ... aber der Effekt ist der gleiche für den Zweck des OP. Ich würde die Einfachheit nicht außer Acht lassen, aber sie ist nicht so effizient wie die foldLeftLösung und auch nicht die eigentliche Antwort auf die Frage "Umwandlung in eine Sammlung in eine Karte nach Schlüssel"
Dexter Legaspi
2

Wie wäre es mit zip und toMap?

myList.zip(myList.map(_.length)).toMap
AwesomeBobX64
quelle
1

Für das, was es wert ist, gibt es zwei sinnlose Möglichkeiten:

scala> case class Foo(bar: Int)
defined class Foo

scala> import scalaz._, Scalaz._
import scalaz._
import Scalaz._

scala> val c = Vector(Foo(9), Foo(11))
c: scala.collection.immutable.Vector[Foo] = Vector(Foo(9), Foo(11))

scala> c.map(((_: Foo).bar) &&& identity).toMap
res30: scala.collection.immutable.Map[Int,Foo] = Map(9 -> Foo(9), 11 -> Foo(11))

scala> c.map(((_: Foo).bar) >>= (Pair.apply[Int, Foo] _).curried).toMap
res31: scala.collection.immutable.Map[Int,Foo] = Map(9 -> Foo(9), 11 -> Foo(11))
fehlender Faktor
quelle
Auch, fwiw, so würden diese beiden in Haskell aussehen: Map.fromList $ map (bar &&& id) c, Map.fromList $ map (bar >>= (,)) c.
Fehlender Faktor
-1

Das funktioniert bei mir:

val personsMap = persons.foldLeft(scala.collection.mutable.Map[Int, PersonDTO]()) {
    (m, p) => m(p.id) = p; m
}

Die Karte muss veränderbar sein und die Karte muss zurückgegeben werden, da das Hinzufügen zu einer veränderlichen Karte keine Karte zurückgibt.

Rostfinger
quelle
1
Tatsächlich kann es unveränderlich wie folgt implementiert werden: val personsMap = persons.foldLeft(Map[Int, PersonDTO]()) { (m, p) => m + (p.id -> p) }Die Karte kann unveränderlich sein, wie oben gezeigt, da das Hinzufügen zu einer unveränderlichen Karte eine neue unveränderliche Karte mit dem zusätzlichen Eintrag zurückgibt. Dieser Wert dient als Akkumulator während der Falzoperation.
RamV13
-2

Verwenden Sie map () für die Sammlung, gefolgt von toMap

val map = list.map(e => (e, e.length)).toMap
Krishna Kumar Chourasiya
quelle
3
Wie unterscheidet sich dies von der Antwort, die vor 7 Jahren eingereicht und akzeptiert wurde?
JWVH
-3

Bei der Konvertierung von Json String (Lesen einer Json-Datei) in Scala Map

import spray.json._
import DefaultJsonProtocol._

val jsonStr = Source.fromFile(jsonFilePath).mkString
val jsonDoc=jsonStr.parseJson
val map_doc=jsonDoc.convertTo[Map[String, JsValue]]

// Get a Map key value
val key_value=map_doc.get("key").get.convertTo[String]

// If nested json, re-map it.
val key_map=map_doc.get("nested_key").get.convertTo[Map[String, JsValue]]
println("Nested Value " + key_map.get("key").get)
Ajit Surendran
quelle
Diese 11 Jahre alte Frage fragt nichts nach JSON. Ihre Antwort ist fehl am Platz.
Jwvh