1. Overview

In this tutorial, we’ll learn about Scala’s Map. We’ll see how to store key-value pairs, how to retrieve, update, and delete a value under a given key. Then we’ll explore a few ways of transforming a Map.

2. Map

Scala’s Map is a collection of key-value pairs, where each key needs to be unique. Thanks to that, we have direct access to a value under a given key.

Scala defines two kinds of maps, the immutable, which is used by default and mutable, which needs an import scala.collection.mutable.Map.

3. Creating a Map

In this section, we’ll take a look at some of the different options we have for creating maps.

3.1. Empty

Let’s start by creating an empty Map, which can be used as a default value. We can use the empty method on the Map companion object:

val emptyMap: Map[Int, String] = Map.empty[Int, String]

Another way of creating an empty Map is by using the apply method:

val emptyMap: Map[Int, String] = Map[Int, String].apply()

Scala has a special syntactic sugar for this which gives us the ability to call it using only the parentheses:

val emptyMap: Map[Int, String] = Map[Int, String]()

3.2. Non-Empty

When we want to create a non-empty Map, we can use the apply method and pass as arguments key-value tuples:

val map: Map[Int, String] = Map.apply(1 -> "first", 2 -> "second")

We can also use syntactic sugar for the apply method:

val map: Map[Int, String] = Map(1 -> "first", 2 -> "second")

Another very popular way of creating a Map is by converting a List into a Map:

val map: Map[Int, String] = List(1 -> "first", 2 -> "second").toMap
map shouldBe Map(1 -> "first", 2 -> "second")

This way of creating a Map is so popular that there is even a dedicated toMap method for it. It only works when our List contains elements of type Tuple2:

val map: Map[Int, String] = List(1, 2).toMap

Otherwise, the compiler will fail with the message:

[error] Cannot prove that Int <:< (T, U). 
[error] val map: Map[Int, String] = List(1, 2).toMap

As we can see, we can only call the toMap method when elements in the List are a subtype of Tuple2, which is not true for Int.

3.3. Inference

Most of the time, we don’t need to specify the types explicitly. Unfortunately, there are some situations in which the compiler cannot infer them correctly. This happens mostly for generic methods with two lists of arguments, like foldLeft or foldRight on List:

List(1 -> "first", 2 -> "second")
  .foldLeft(Map.empty) {
    case (map, (key, value)) =>
      map + (key -> value)
  }

The above code tries to fold a List of Tuples2 into a Map starting with an empty one. Unfortunately, it won’t compile, because we didn’t explicitly specify the types when using Map.empty and the compiler inferred them as Nothing:

[error] type mismatch;
[error]  found   : (Int, String)
[error]  required: (Nothing, Nothing)
[error]             map + (key -> value)

If we specify them, the code will compile without any errors:

List(1 -> "first", 2 -> "second")
  .foldLeft(Map.empty[Int, String]) {
    case (map, (key, value)) =>
      map + (key -> value)
  }

4. Adding Elements

Right now we know how to create a Map, so it’s time to see how to add new elements into a Map:

val initialMap: Map[Int, String] = Map(1 -> "first")
val newMap: Map[Int, String] = initialMap + (2 -> "second")

The above code creates a new Map using the + (alias for updated) method containing all the elements from initialMap plus a new key-value pair. We should remember that the initial Map stays unchanged.

Let’s now verify that the content of our newMap is as expected:

initialMap shouldBe Map(1 -> "first")
newMap shouldBe Map(1 -> "first", 2 -> "second")

Using the same technique, we can also add multiple key-value pairs:

val initialMap: Map[Int, String] = Map(1 -> "first")
val newMap: Map[Int, String] = initialMap + (2 -> "second", 3 -> "third")

initialMap shouldBe Map(1 -> "first")
newMap shouldBe Map(1 -> "first", 2 -> "second", 3 -> "third")

5. Merging Maps

We can merge two maps together using the ++ (alias for concat) method:

val leftMap: Map[Int, String] = Map(1 -> "first", 2 -> "second")
val rightMap: Map[Int, String] = Map(2 -> "2nd", 3 -> "third")

val map = leftMap ++ rightMap

map shouldBe Map(1 -> "first", 2 -> "2nd", 3 -> "third")

We see that the rightMap on the right-hand side of ++ overrides the leftMap values for the same keys.

It’s also possible to merge maps using a List of pairs:

val leftMap: Map[Int, String] = Map(1 -> "first", 2 -> "second")
val list: List[(Int, String)] = List(2 -> "2nd", 3 -> "third")

val map = leftMap ++ list
map shouldBe Map(1 -> "first", 2 -> "2nd", 3 -> "third")

6. Overriding Values

We’ve already mentioned that keys in a Map need to be unique. It’s time to check what happens when we add  a new value to a Map under an already existing key:

val initialMap: Map[Int, String] = Map(1 -> "first")
val newMap = initialMap + (1 -> "1st")

newMap shouldBe Map(1 -> "1st")

We can see that putting a new value under an existing key overrides its previous value.

7. Getting Values

In this section, we’ll take a look at some of the options we have for retrieving values from a Map:

7.1. get

Now we know how to put elements into a Map. Now it’s time to check how to get a value for a given key. For this purpose, we can use the get method:

val map: Map[Int, String] = Map(1 -> "first", 2 -> "second")

map.get(1) shouldBe Some("first")
map.get(3) shouldBe None

We should notice that this method returns us the value inside an Option because there is a possibility that the given key doesn’t exist.

7.2. apply

Another way of retrieving a value is by using the apply method:

val map: Map[Int, String] = Map(1 -> "first", 2 -> "second")

map.apply(1) shouldBe "first"

Unfortunately, this method doesn’t wrap its result into Option, but it throws an exception when a given key doesn’t exist:

the[NoSuchElementException] thrownBy map.apply(3)

We can also use the apply syntactic sugar:

map(1) shouldBe "first"

7.3. withDefaultValue

If we have some reasonable default for when no value exists for a given key, we can use the withDefaultValue method:

val map: Map[Int, String] = Map(1 -> "first", 2 -> "second")
val mapWithDefault: Map[Int, String] = map.withDefaultValue("unknown")

In this example, we’ve created a new Map, that returns “unknown” for missing keys. We can check this behavior by getting some values from it:

mapWithDefault(1) shouldBe "first"
mapWithDefault(3) shouldBe "unknown"

7.4. withDefault

There is also an option to compute values based on the missing key using the withDefault method:

val map: Map[Int, String] = Map(1 -> "first", 2 -> "second")
val mapWithDefault: Map[Int, String] = map.withDefault(i => i + "th")

This time, we’ve created a new Map, that calculates values (adding the “th” suffix)  for missing keys:

mapWithDefault(1) shouldBe "first"
mapWithDefault(5) shouldBe "5th"

8. Removing Keys

Now let’s take a look at how we can remove keys.

8.1. Single Key

We already know how to add, update, and get values from a Map. To remove a key from a Map, we can use the (alias for removed) method:

val initialMap: Map[Int, String] = Map(1 -> "first", 2 -> "second")
val newMap: Map[Int, String] = initialMap - 1

It returns a new Map that doesn’t contain the key we just removed. If the given key doesn’t exist in our Map then it returns the initial Map:

newMap shouldBe Map(2 -> "second")

The initialMap remains unchanged:

initialMap shouldBe Map(1 -> "first", 2 -> "second")

8.2. Multiple Keys

We can also remove multiple keys using the same method:

val initialMap: Map[Int, String] = Map(1 -> "first", 2 -> "second")
val newMap: Map[Int, String] = initialMap - (1, 2, 3)

initialMap shouldBe Map(1 -> "first", 2 -> "second")
newMap shouldBe empty

8.3. List of Keys

Another option to remove keys is calling the (alias for removedAll) method with List of keys:

val initialMap: Map[Int, String] = Map(1 -> "first", 2 -> "second")
val newMap: Map[Int, String] = map -- List(1, 2)

initialMap shouldBe Map(1 -> "first", 2 -> "second")
newMap shouldBe empty

9. Transforming a Map

In this final section, we’ll see how we can transform maps.

9.1. map

Map, as any other Scala collection, can be transformed by passing a function into the map method:

val initialMap: Map[Int, String] = Map(1 -> "first", 2 -> "second")

val abbreviate: ((Int, String)) => (Int, String) = {
  case (key, value) =>
    val newValue = key + value.takeRight(2)
    key -> newValue
}

In this example, we’ve created an abbreviate function that takes the last two characters of the value and concatenates them with the key. We can pass it into the map method to abbreviate all values in initialMap:

val abbreviatedMap = initialMap.map(abbreviate)

initialMap shouldBe Map(1 -> "first", 2 -> "second")
abbreviatedMap shouldBe Map(1 -> "1st", 2 -> "2nd")

It applies the abbreviate function to each key-value pair in initialMap and returns a new Map that contains the results. The initialMap remains unchanged.

9.2. mapValues

If we want to transform only values, we can use the mapValues method and pass a mapping function into it:

val initialMap: Map[Int, String] = Map(1 -> "first", 2 -> "second")
val reverse: String => String = value => value.reverse

val reversed: Map[Int, String] = initialMap.mapValues(reverse)

reversed.get(1) shouldBe Some("tsrif")
reversed.get(2) shouldBe Some("dnoces")

Unfortunately, this method has a caveat. It doesn’t apply a mapping function for all the values in our map when we call it but evaluates it each time we want to get a value.

Let’s add a counter into the mapping function to make it easier to understand:

val initialMap: Map[Int, String] = Map(1 -> "first", 2 -> "second")

val counter = new AtomicInteger(0)
val reverse: String => String = { value =>
  counter.incrementAndGet()
  value.reverse
}

val reversed: Map[Int, String] = initialMap.mapValues(reverse)

Now let’s verify the behavior of our counter:

counter.get() shouldBe 0

reversed.get(1) shouldBe Some("tsrif")
counter.get() shouldBe 1

reversed.get(2) shouldBe Some("dnoces")
counter.get() shouldBe 2

reversed.get(1) shouldBe Some("tsrif")
counter.get() shouldBe 3

As we can see, calling mapValues doesn’t change our counter. The counter changes only when we want to retrieve values.

If we want to make the mapping strict, we should call the view and force methods after we call the mapValues method:

val initialMap: Map[Int, String] = Map(1 -> "first", 2 -> "second")

val counter = new AtomicInteger(0)
val reverse: String => String = { value =>
  counter.incrementAndGet()
  value.reverse
}

val reversed: Map[Int, String] = initialMap
  .mapValues(reverse)
  .view
  .force

counter.get() shouldBe map.size

reversed.get(1) shouldBe Some("tsrif")
counter.get() shouldBe map.size

reversed.get(2) shouldBe Some("dnoces")
counter.get() shouldBe map.size

We should also note that since Scala 2.13, the mapValues method is deprecated.

9.3. filter

We can also filter the elements of a map by passing a predicate to the filter method.

Let’s define some predicate function that will return true only when a key is greater than one, and the value is longer than five:

val predicate: ((Int, String)) => Boolean = {
  case (key, value) => key > 1 && value.length > 5
}

Right now we can pass our predicate function into the filter method:

val initialMap: Map[Int, String] = Map(1 -> "first", 2 -> "second")
val filtered: Map[Int, String] = initialMap.filter(predicate)

It applies our predicate to each key-value pair in the initialMap and returns a new Map that contains only pairs for which the predicate returns true:

filtered shouldBe Map(2 -> "second")

9.4. filterKeys

If we want to filter Map elements only by key, we should use the filterKeys method.

Again, we can create a simple predicate function that will return true only when a given key is greater than 1:

val predicate: Int => Boolean = key => key > 1

Let’s pass it to the filterKeys method:

val initialMap: Map[Int, String] = Map(1 -> "first", 2 -> "second")
val filtered: Map[Int, String] = initialMap.filterKeys(predicate)

As before, it applies our predicate function to each key in the initialMap and returns a new Map that contains only elements for which predicate returns true:

filtered.get(1) shouldBe None
filtered.get(2) shouldBe Some("second")

Unfortunately, it behaves in the same way as the mapValues method, and it’s also deprecated since Scala 2.13.

 10. Conclusion

In this article, we’ve explored the basics of Scala’s Map API

We learned how to create an empty and non-empty Map and how to get, update, and remove values. Then we saw how to transform a Map using different methods and pointed out some of their caveats.

As always, the full source code of the article is available over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments