1. Introduction

Scala’s Map is a powerful data structure to store key/value pairs and model dictionaries. Keys in a Map are unique and allow us to have direct access to the corresponding value. Furthermore, the standard library offers a few ways to map both keys and values*.*

In this tutorial, we’re going to see a few ways to apply a transformation to the keys and the values.

2. Mapping Functions

There are three main ways to transform both keys and values of a Scala Map[K, V], where K is the type of the keys and V is that of the values. These methods are Map::map, Map::flatMap, and Map::transform.

2.1. Map::map

Let’s first take a look at the common map function:

def map[B](f: ((K, V)) => B): Iterable[B]

Map::map builds a new Iterable by applying the function f to all the elements, seen as key/value pairs. As we can see from its signature, f returns a single value of type B for each key/value pair. Hence, the return value of Map::map will be an Iterable[B]. Let’s see an example:

val m = Map(1 -> "A", 2 -> "B")
val f = { t: (Int, String) => s"${t._1}${t._2}" }
(m map f) shouldBe Iterable("1A", "2B")

In the example above, we started with a Map[Int, String] and defined a function f of type ((Int, String)) => String, that is, a function from a pair (Int, String) to a String. The fact that the parameter t is a tuple is clear by looking at how we access its elements: t._1 for the key in the Map and t._2 for the corresponding value. Once we map the keys and the values, we obtain a new Iterable with elements of type String.

If f was a binary function, we couldn’t provide it immediately to Map::map. In this case, we’d have to use f.tupled instead:

val m = Map(1 -> "A", 2 -> "B")
val f = { (k: Int, v: String) => s"$k$v" }
(m map f.tupled) shouldBe Iterable("1A", "2B")

f.tupled turns a binary function into a unary one whose input is a pair.

Map::map is an overloaded method. As a matter of fact, there exists another version that preserves the Map type in the return value:

def map[K2, V2](f: ((K, V)) => (K2, V2)): Map[K2, V2]

The function above is similar to the one we’ve seen before, with one key difference: the mapping function f returns a new pair (K2, V2) instead of a single value. Hence, the return type can be a new Map[K2, V2]:

val m: Map[Int, String] = Map(1 -> "A", 2 -> "BB")
val newMap: Map[String, Int] = m map { 
  case (k, v) => (k.toString, v.length)
}
newMap shouldBe Map("1" -> 1, "2" -> 2)

In the example above, we leveraged some Scala syntactic sugar and destructed the tuple using pattern-matching. In the following examples, we’ll continue to use this style instead of specifying the mapping function as we did above.

The key difference in the last example is that the function applied to m to obtain newMap returns a new pair, where the key is computed from the original one (by turning the number into a String) and the corresponding value is obtained by counting the length of the original one.

Hence, in the final assertion, we can specify Map as a type, instead of a generic Iterable. *Furthermore, the explicit types of m and newMap clarify that the two values are both Maps.*

If the mapping function returns two (or more) pairs with the same key, only the last one will be added to the resulting Map. This is because, as we saw above, keys in a Map must be unique:

val m: Map[Int, String] = Map(1 -> "A", 2 -> "BB")
val newMap: Map[Int, Int] = m map { case (_, v) => (1, v.length) }
newMap shouldBe Map(1 -> 2)

2.2. Map::flatMap

The second method to map both keys and values is Map::flatMap. As with other Scala collections, flatMap flattens the collections returned by the mapping function into a single, flat one. As before, Map::flatMap comes in two flavors, as the mapping function can either return a single element or a pair:

def flatMap[K2, V2](f: ((K, V)) => IterableOnce[(K2, V2)]): Map[K2, V2]
def flatMap[B](f: ((K, V)) => IterableOnce[B]): Iterable[B]

According to the two signatures, the only difference with respect to Map::map is that the function f now returns an IterableOnce. Let’s see how we can use Map::flatMap in practice:

val m = Map(1 -> "A", 2 -> "B", 3 -> "C")

val newIterable: Iterable[String] = m flatMap {
  case (k, v) => List.fill(k)(v)
}
newIterable shouldBe Iterable("A", "B", "B", "C", "C", "C")

val newMap: Map[Int, String] = m flatMap {
  case (k, v) => (1 to k).map(i => i -> s"$i$v")
}
newMap shouldBe Map(1 -> "C", 2 -> "CC", 3 -> "CCC")

In the first part of the example above, we build a new instance of Iterable[String] using Map::flatMap. In particular, for each key/value pair, we build a List by repeating the value of the pair a number of times represented by the key. As usual, flatMap flattens the resulting collection into Iterable(“A”, “B”, “B”, “C”, “C”, “C”).

*In the second part of the example, we use Map::flatMap to build a new Map[Int, String].* The transformation function is a bit more complex in this case.

Essentially, given k as the value of each key in the original map, we return k new pairs. The first element of each pair is an increasing index i from 1 to k. The second element, instead, is simply the value in the original pair with the index i as a prefix. For example, the transformation function applied to the second pair of the original map (2 -> “B”) will produce Iterable(1 -> “1B”, 2 -> “2B”).

Map::flatMap also takes care of removing duplicate pairs. Hence, the final result will be Map(1 -> “1C”, 2 -> “2C”, 3 -> “3C”). This is because the keys produced by the first two iterations, on the pairs (1 -> “A” and 2 -> “B”), are overwritten by those produced by the pair 3 ->”C”.

2.3. Map::transform

Another way to map keys and values in a Scala Map is Map::transform. Given a Map[K, V], it allows us to build a Map[K, W], where we can produce the new values (of type W) by taking into account both the key and the value of each element in the original Map:

def transform[W](f: (K, V) => W): Map[K, W]

This is different from just mapping the values of the map, as the transformation function can access the key of each element as well:

val m: Map[Int, Char] = Map(1 -> 'A', 2 -> 'B', 3 -> 'C')

val newMap: Map[Int, String] = m transform {
  case (k, v) => s"$k$v"
}
newMap shouldBe Map(1 -> "1A", 2 -> "2B", 3 -> "3C")

In the example above, we start with a Map[Int, Char] and we want to produce a new map by keeping the original keys intact. However, we want to prepend the key to the original value in the new map. We can use Map::transform to this end. The result is a Map[Int, String], as expected.

3. Conclusion

In this article, we saw how to map both keys and values of a Scala Map. In particular, we analyzed three different ways — Map::map, Map::flatMap, and Map::transform. Additionally, we investigated the two overloads of map and flatMap and compared their signatures.

As usual, the sample code is available over on GitHub.


» 下一篇: Scala 继承指南