Scala é a melhor maneira de transformar uma coleção em um mapa por chave?

165

Se eu tiver uma coleção cde tipos Te houver uma propriedade pem T(do tipo P, digamos), qual é a melhor maneira de fazer uma chave de mapeamento por extração ?

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

Uma maneira é a seguinte:

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

Mas agora eu preciso de um mapa mutável . Existe uma maneira melhor de fazer isso para que fique em uma linha e acabe com um mapa imutável ? (Obviamente, eu poderia transformar o anterior em um utilitário de biblioteca simples, como faria em Java, mas suspeito que no Scala não há necessidade)

oxbow_lakes
fonte

Respostas:

232

Você pode usar

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

mas lembre-se de que isso precisa de 2 travessias.

Ben Lings
fonte
8
Ainda prefiro minhas sugestões no trac de a Traversable[K].mapTo( K => V)e Traversable[V].mapBy( V => K)foram melhores!
oxbow_lakes
7
Esteja ciente de que esta é uma operação quadrática, mas o mesmo vale para a maioria das outras variantes fornecidas aqui. Observando o código fonte de scala.collection.mutable.MapBuilder etc, parece-me que, para cada tupla, um novo mapa imutável é criado ao qual a tupla é adicionada.
Jcsahnwaldt Restabelece Monica 03/03
30
Na minha máquina para uma lista com 500.000 elementos, esse código Scala é cerca de 20 vezes mais lento que a abordagem Java direta (crie HashMap com tamanho apropriado, faça uma lista de loop over, coloque elementos no mapa). Para 5.000 elementos, o Scala é cerca de 8 vezes mais lento. A abordagem de loop escrita no Scala é aproximadamente três vezes mais rápida que a variante toMap, mas ainda entre 2 e 7 vezes mais lenta que o Java.
Jcsahnwaldt Restabelece Monica
8
Você poderia fornecer as fontes de teste para a comunidade SO? THX.
User573215
8
Substitua cpor c.iteratorpara evitar a criação de coleção intermediária.
ghik
21

Você pode construir um mapa com um número variável de tuplas. Portanto, use o método map na coleção para convertê-lo em uma coleção de tuplas e, em seguida, use o truque: _ * para converter o resultado em um argumento variável.

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
fonte
5
Estive lendo sobre o Scala por mais de duas semanas e trabalhando em exemplos e nunca vi essa notação ": _ *"! Muito obrigado pela sua ajuda
oxbow_lakes
Só para constar, pergunto-me por que precisamos precisar que essa é uma sequência com _ . O mapa ainda converter retorna uma lista de tupla aqui. Então, por que o _ ? Quero dizer ele funciona, mas eu gostaria de entender o tipo de atribuição aqui
MaatDeamon
1
Isso é mais eficiente que os outros métodos?
precisa saber é o seguinte
16

Além da solução de @James Iry, também é possível fazer isso usando uma dobra. Suspeito que esta solução seja um pouco mais rápida que o método tupla (menos objetos de lixo são criados):

val list = List("this", "maps", "string", "to", "length")
val map = list.foldLeft(Map[String, Int]()) { (m, s) => m(s) = s.length }
Daniel Spiewak
fonte
Vou tentar isso (tenho certeza que funciona :-). O que está acontecendo com a função "(m, s) => m (s) = s.length"? Eu vi o exemplo típico de foldLeft com uma soma e uma função "_ + _"; isso é muito mais confuso! A função parece supor que eu já tenho uma tupla (m, s), o que eu realmente não entendo
oxbow_lakes
2
Cara, Scala era estranho naquela época!
missingfaktor
8
@ Daniel Eu tento seu código, mas aparece o seguinte erro: "atualização de valor não é membro de scala.collection.immutable.Map [String, Int]". Por favor, explique seu código como trabalhar com esse código?
mr.boyfox
1
não parece para mim work.For quer "Aplicação não leva parâmetros"
jayunit100
7
Versão imutável: list.foldLeft(Map[String,Int]()) { (m,s) => m + (s -> s.length) }. Note que se você quiser usar a vírgula para construir a tupla, você precisa de um par extra de parênteses: ((s, s.length)).
Kelvin
11

Isso pode ser implementado de forma imutável e com uma única passagem, dobrando a coleção da seguinte maneira.

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

A solução funciona porque a adição a um mapa imutável retorna um novo mapa imutável com a entrada adicional e esse valor serve como acumulador na operação de dobra.

A desvantagem aqui é a simplicidade do código versus sua eficiência. Portanto, para coleções grandes, essa abordagem pode ser mais adequada do que usar 2 implementações transversais, como aplicar mape toMap.

RamV13
fonte
8

Outra solução (pode não funcionar para todos os tipos)

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

isso evita a criação da lista intermediária, mais informações aqui: Scala 2.8 breakOut

Somatik
fonte
7

O que você está tentando alcançar é um pouco indefinido.
E se dois ou mais itens ccompartilharem o mesmo p? Qual item será mapeado para aquele pno mapa?

A maneira mais precisa de ver isso é produzir um mapa entre pe todos os citens que o contêm:

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

Isso pode ser facilmente alcançado com groupBy :

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

Se você ainda deseja o mapa original, pode, por exemplo, mapear ppara o primeiro tque o possui:

val m: Map[P, T] = c.groupBy(t => t.p) map { case (p, ts) =>  p -> ts.head }
Eyal Roth
fonte
1
Um ajuste prático nisso é usar em collectvez de map. Por exemplo: c.group(t => t.p) collect { case (Some(p), ts) => p -> ts.head }. Dessa forma, você pode fazer coisas como nivelar mapas quando digitar é uma opção [_].
healsjnr
@healsjnr Claro, isso pode ser dito para qualquer mapa. Não é a questão central aqui, no entanto.
Eyal Roth
1
Você poderia usar em .mapValues(_.head)vez do mapa.
Lex82 3/04
2

Provavelmente, essa não é a maneira mais eficiente de transformar uma lista em mapa, mas torna o código de chamada mais legível. Usei conversões implícitas para adicionar um método mapBy à List:

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

Exemplo de código de chamada:

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

Observe que, devido à conversão implícita, o código do chamador precisa importar as implitConversions do scala.

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

Funciona bem e é muito intuitivo

Jörg Bächtiger
fonte
8
Por favor, adicione mais detalhes.
Syeda Zunaira 4/04
2
Eu sinto Muito. Mas, esta é uma resposta para a pergunta "Scala é a melhor maneira de transformar uma coleção em um mapa por chave?" como Ben Lings é.
Jörg Bächtiger
1
E Ben não deu nenhuma explicação?
Shinzou
1
isso cria duas listas e combina em um "mapa" usando os elementos ccomo chave (mais ou menos). Observe "map" porque a coleção resultante não é uma escala, Mapmas cria outra lista / iterável de tuplas ... mas o efeito é o mesmo para o objetivo do OP, não descartaria a simplicidade, mas não é tão eficiente quanto a foldLeftsolução, nem é a verdadeira resposta para a pergunta "convertendo em uma coleção em um mapa-by-key"
Dexter Legaspi
2

Que tal usar zip e toMap?

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

Pelo que vale, aqui estão duas maneiras inúteis de fazer isso:

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))
desaparecido
fonte
Além disso, fwiw, é assim que esses dois ficariam em Haskell: Map.fromList $ map (bar &&& id) c, Map.fromList $ map (bar >>= (,)) c.
missingfaktor
-1

Isso funciona para mim:

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

O mapa deve ser mutável e o mapa deve ser retornado, pois a adição a um mapa mutável não retorna um mapa.

enferrujado
fonte
1
Na verdade, ele pode ser implementado de forma imutável da seguinte maneira: val personsMap = persons.foldLeft(Map[Int, PersonDTO]()) { (m, p) => m + (p.id -> p) }O Mapa pode ser imutável, como evidenciado acima, porque adicionar a um Mapa imutável retorna um novo Mapa imutável com a entrada adicional. Este valor serve como acumulador através da operação de dobra.
RamV13
-2

use map () na coleção seguido com toMap

val map = list.map(e => (e, e.length)).toMap
Krishna Kumar Chourasiya
fonte
3
Como isso difere da resposta que foi enviada e aceita há 7 anos?
jwvh
-4

Se estiver convertendo de Json String (lendo um arquivo json) em 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
fonte
Esta pergunta de 11 anos não pergunta nada sobre JSON. Sua resposta está fora do lugar.
jwvh 30/06