178

If I have a collection c of type T and there is a property p on T (of type P, say), what is the best way to do a map-by-extracting-key?

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

One way is the following:

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

But now I need a mutable map. Is there a better way of doing this so that it's in 1 line and I end up with an immutable Map? (Obviously I could turn the above into a simple library utility, as I would in Java, but I suspect that in Scala there is no need)

Eugene Yokota
  • 94,654
  • 45
  • 215
  • 319
oxbow_lakes
  • 133,303
  • 56
  • 317
  • 449

13 Answers13

255

You can use

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

but be aware that this needs 2 traversals.

Ben Lings
  • 28,823
  • 13
  • 72
  • 81
  • 8
    I still prefer my suggestions in trac of a `Traversable[K].mapTo( K => V)` and `Traversable[V].mapBy( V => K)` were better! – oxbow_lakes Jul 14 '10 at 21:17
  • As alternative, with zip: `c map (_.getP) zip c toMap` – onof May 14 '11 at 15:00
  • 7
    Be aware that this is a quadratic operation, but the same goes for most other variants given here. Looking at the source code of scala.collection.mutable.MapBuilder etc, it seems to me that for each tuple, a new immutable map is created to which the tuple is added. – jcsahnwaldt Reinstate Monica Mar 03 '12 at 02:17
  • 31
    On my machine for a list with 500,000 elements, this Scala code is about 20 times slower than the straight-forward Java approach (create HashMap with appropriate size, loop over list, put elements into map). For 5,000 elements, Scala ist about 8 times slower. The loop approach written in Scala is roughly 3 times faster than the toMap variant, but still between 2 and 7 times slower than Java. – jcsahnwaldt Reinstate Monica Mar 11 '12 at 02:13
  • 8
    Would you please provide the test sources to the SO community? Thx. – user573215 Sep 23 '13 at 09:11
  • 8
    Replace `c` with `c.iterator` to avoid creation of intermediate collection. – ghik Aug 16 '14 at 11:14
  • @JonaChristopherSahnwaldt Do you have results about performance with Java 8 and lambdas? :) – Cherry Mar 06 '15 at 10:01
  • @Cherry Good question! In my microbenchmarks, code with or without streams and lamdas has the exact same performance (within measurable precision). Initializing the map with the correct size (including load factor!) makes the process two times faster in each case. The syntax for the initializing the map is a bit awkward with streams. https://gist.github.com/jcsahnwaldt/f904de89a4440a1b0458 – jcsahnwaldt Reinstate Monica Mar 09 '15 at 02:43
  • @user573215 Sorry, I can't find the sources anymore. – jcsahnwaldt Reinstate Monica Mar 09 '15 at 02:44
  • The suggestion with breakOut does not work in 2.10 (compiler complains that ```Iterator[(P,T)] does not take parameters```. However, both suggestions work in Scala 2.11. – Taylor R Dec 02 '15 at 17:22
24

You can construct a Map with a variable number of tuples. So use the map method on the collection to convert it into a collection of tuples and then use the : _* trick to convert the result into a variable argument.

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
  • 19,367
  • 3
  • 64
  • 56
  • 5
    I've been reading about Scala for >2 weeks and working through examples and not once had I seen this ": _ *" notation! Thanks very much for your help – oxbow_lakes Mar 23 '09 at 21:15
  • Just for the record, i wonder why we need to precise that this is a sequence with _*. map still convert return a list of tuple here. So why the _* ? I mean it works but i would like to understand the type ascription here – MaatDeamon Jul 09 '15 at 20:18
  • 1
    Is this more efficient than the other methods? – Jus12 Aug 22 '15 at 08:14
20

In addition to @James Iry's solution, it is also possible to accomplish this using a fold. I suspect that this solution is slightly faster than the tuple method (fewer garbage objects are created):

val list = List("this", "maps", "string", "to", "length")
val map = list.foldLeft(Map[String, Int]()) { (m, s) => m(s) = s.length }
Daniel Spiewak
  • 54,515
  • 14
  • 108
  • 120
  • I will try this out (I'm sure it works :-). What is going on with the "(m,s)=>m(s) = s.length" function? I have seen the typical foldLeft example with a sum and a function "_ + _"; this is much more confusing! The function seems to assume that I already have a tuple (m,s), which I don't really get – oxbow_lakes Mar 24 '09 at 22:33
  • *Is* this right? According to the scaladoc of foldLeft: "foldLeft [B](z : B)(op : (B, A) => B) : B" B in this case must be a Map[String, Int], so I don't really understand the function in your example at all! It should return a Map for a start, shouldn't it? – oxbow_lakes Mar 24 '09 at 22:50
  • OK - so I've got this! "m(s) = s.length" (where m is a map) returns a new map with the mapping "s -> s.length". How was I supposed to know this? I can't find it anywhere in the programming in scala sections on maps! – oxbow_lakes Mar 25 '09 at 09:52
  • That is scala's syntactic sugar for update: An assignment f(args) = e with a function application to the left of the '=' operator is interpreted as f.update(args, e), i.e. the invocation of an update function defined by f. [The Scala Language Specification Version 2.7, 6.15 Assignments] – Palimondo Mar 18 '10 at 01:15
  • 2
    Man, Scala was weird back then! – missingfaktor Feb 04 '12 at 09:53
  • 9
    @Daniel I try your code, but appear following error: "value update is not a member of scala.collection.immutable.Map[String,Int]". Please explain your code how to working this code? – SBotirov Feb 18 '14 at 15:15
  • 1
    doesnt seem to work.for me either "Application does not take parameters" – jayunit100 Feb 15 '15 at 22:05
  • @jayunit100 This is because fold left has to return a value after each iteration as well. This should work. `list.foldLeft(Map[String, Int]()) { (m, s) => m(s) = s.length; m }` Also sorry for being 10 months too late :-) – Stephen Carman Dec 04 '15 at 18:32
  • 7
    Immutable version: `list.foldLeft(Map[String,Int]()) { (m,s) => m + (s -> s.length) }`. Note that if you want to use comma to build the tuple, you need an extra pair of parentheses: `((s, s.length))`. – Kelvin Apr 18 '17 at 23:04
13

This can be implemented immutably and with a single traversal by folding through the collection as follows.

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

The solution works because adding to an immutable Map returns a new immutable Map with the additional entry and this value serves as the accumulator through the fold operation.

The tradeoff here is the simplicity of the code versus its efficiency. So, for large collections, this approach may be more suitable than using 2 traversal implementations such as applying map and toMap.

RamV13
  • 547
  • 4
  • 8
9

Another solution (might not work for all types)

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

this avoids the creation of the intermediary list, more info here: Scala 2.8 breakOut

Community
  • 1
  • 1
Somatik
  • 4,723
  • 3
  • 37
  • 49
8

What you're trying to achieve is a bit undefined.
What if two or more items in c share the same p? Which item will be mapped to that p in the map?

The more accurate way of looking at this is yielding a map between p and all c items that have it:

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

This could be easily achieved with groupBy:

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

If you still want the original map, you can, for instance, map p to the first t that has it:

val m: Map[P, T] = c.groupBy(t => t.p) map { case (p, ts) =>  p -> ts.head }
Eyal Roth
  • 3,895
  • 6
  • 34
  • 45
  • 1
    One handy tweak on this is to use ```collect``` instead of ```map```. Eg: ```c.group(t => t.p) collect { case (Some(p), ts) => p -> ts.head }```. This way you can do things like flatten maps when you key is an Option[_]. – healsjnr Feb 16 '16 at 04:54
  • @healsjnr Sure, this could be said for any map. It isn't the core issue here, though. – Eyal Roth Feb 16 '16 at 10:39
  • 1
    You could use `.mapValues(_.head)` instead of the map. – lex82 Apr 03 '18 at 15:37
6

Scala 2.13+

instead of "breakOut" you could use

c.map(t => (t.getP, t)).to(Map)

Scroll to "View": https://www.scala-lang.org/blog/2017/02/28/collections-rework.html

happysathya
  • 142
  • 2
  • 9
RonRuby
  • 61
  • 1
  • 1
2

This is probably not the most efficient way to turn a list to map, but it makes the calling code more readable. I used implicit conversions to add a mapBy method to 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
  }
}

Calling code example:

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

Note that because of the implicit conversion, the caller code needs to import scala's implicitConversions.

Erez
  • 29
  • 1
2
c map (_.getP) zip c

Works well and is very intuitiv

  • 8
    Please add more details. – Syeda Zunaira Dec 04 '14 at 10:57
  • 2
    I'm sorry. But, this IS an answer to the question "Scala best way of turning a Collection into a Map-by-key?" as Ben Lings is. – Jörg Bächtiger Dec 15 '14 at 15:55
  • 1
    And Ben didn't provide any explanation? – shinzou Nov 07 '17 at 12:41
  • 1
    this creates two lists and combine into a "map" using the elements in `c` as key (sort of). Note "map" because the resulting collection is not a scala `Map` but creates another list/iterable of tuples...but the effect is the same for the OP's purpose i wouldn't discount the simplicity but it's not as efficient as `foldLeft` solution, nor it's the real answer to the question "converting into a collection into a map-by-key" – Dexter Legaspi Feb 11 '18 at 14:14
2

How about using zip and toMap?

myList.zip(myList.map(_.length)).toMap
AwesomeBobX64
  • 327
  • 2
  • 4
1

For what it's worth, here are two pointless ways of doing it:

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))
missingfaktor
  • 90,905
  • 62
  • 285
  • 365
  • Also, fwiw, this is how those two would look in Haskell: `Map.fromList $ map (bar &&& id) c`, `Map.fromList $ map (bar >>= (,)) c`. – missingfaktor Feb 04 '12 at 10:08
-1

This works for me:

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

The Map has to be mutable and the Map has to be return since adding to a mutable Map does not return a map.

giampaolo
  • 6,906
  • 5
  • 45
  • 73
rustyfinger
  • 199
  • 2
  • 7
  • 1
    Actually, it can be implemented immutably as follows: ```val personsMap = persons.foldLeft(Map[Int, PersonDTO]()) { (m, p) => m + (p.id -> p) }``` The Map can be immutable, as evidenced above, because adding to an immutable Map returns a new immutable Map with the additional entry. This value serves as the accumulator through the fold operation. – RamV13 Dec 13 '16 at 17:43
-2

use map() on collection followed with toMap

val map = list.map(e => (e, e.length)).toMap
Krishna Kumar Chourasiya
  • 2,030
  • 3
  • 20
  • 23
  • 3
    How is this different from the answer that was submitted, and accepted, 7 years ago? – jwvh Dec 24 '17 at 14:51