# Building Maps

Maps are extremely useful programming tools, and there are numerous ways to construct them.

To create a repeatable set of data, we use the technique shown in [Manipulating Lists](javascript:void(0)), where two Lists are zipped and the result is used in a lambda to call a constructor, producing a List<Person>:

// BuildingMaps/People.kt
package buildingmaps

data class Person(
  val name: String,
  val age: Int
)

val names = listOf("Alice", "Arthricia",
  "Bob", "Bill", "Birdperson", "Charlie",
  "Crocubot", "Franz", "Revolio")

val ages = listOf(21, 15, 25, 25, 42, 21,
  42, 21, 33)

fun people(): List<Person> =
  names.zip(ages) { name, age ->
    Person(name, age)
  }

A Map uses keys to provide fast access to its values. By building a Map with age as the key, we can quickly look up groups of people by age. The library function groupBy() is one way to create such a Map:

// BuildingMaps/GroupBy.kt
import buildingmaps.*
import atomictest.eq

fun main() {
  val map: Map<Int, List<Person>> =
    people().groupBy(Person::age)
  map[15] eq listOf(Person("Arthricia", 15))
  map[21] eq listOf(
    Person("Alice", 21),
    Person("Charlie", 21),
    Person("Franz", 21))
  map[22] eq null
  map[25] eq listOf(
    Person("Bob", 25),
    Person("Bill", 25))
  map[33] eq listOf(Person("Revolio", 33))
  map[42] eq listOf(
    Person("Birdperson", 42),
    Person("Crocubot", 42))
}

groupBy()’s parameter produces a Map where each key connects to a List of elements. Here, all people of the same age are selected by the age key.

You can produce the same groups using the filter() function, but groupBy() is preferable because it only performs the grouping once. With filter() you must repeat the grouping for each new key:

// BuildingMaps/GroupByVsFilter.kt
import buildingmaps.*
import atomictest.eq

fun main() {
  val groups =
    people().groupBy { it.name.first() }
  // groupBy() produces map-speed access:
  groups['A'] eq listOf(Person("Alice", 21),
    Person("Arthricia", 15))
  groups['Z'] eq null

  // Must repeat filter() for each character:
  people().filter {
    it.name.first() == 'A'
  } eq listOf(Person("Alice", 21),
    Person("Arthricia", 15))
  people().filter {
    it.name.first() == 'F'
  } eq listOf(Person("Franz", 21))

  people().partition {
    it.name.first() == 'A'
  } eq Pair(
    listOf(Person("Alice", 21),
      Person("Arthricia", 15)),
    listOf(Person("Bob", 25),
      Person("Bill", 25),
      Person("Birdperson", 42),
      Person("Charlie", 21),
      Person("Crocubot", 42),
      Person("Franz", 21),
      Person("Revolio", 33)))
}

Here, groupBy() groups people() by their first character, selected by first(). We can also use filter() to produce the same result by repeating the lambda code for each character.

If you only need two groups, the partition() function is more direct because it divides the contents into two lists based on a predicate. groupBy() is appropriate when you need more than two resulting groups.

associateWith() allows you to take a list of keys and build a Map by associating each of these keys with a value created by its parameter (here, the lambda):

// BuildingMaps/AssociateWith.kt
import buildingmaps.*
import atomictest.eq

fun main() {
  val map: Map<Person, String> =
    people().associateWith { it.name }
  map eq mapOf(
    Person("Alice", 21) to "Alice",
    Person("Arthricia", 15) to "Arthricia",
    Person("Bob", 25) to "Bob",
    Person("Bill", 25) to "Bill",
    Person("Birdperson", 42) to "Birdperson",
    Person("Charlie", 21) to "Charlie",
    Person("Crocubot", 42) to "Crocubot",
    Person("Franz", 21) to "Franz",
    Person("Revolio", 33) to "Revolio")
}

associateBy() reverses the order of association produced by associateWith()—the selector (the lambda in the following example) becomes the key:

// BuildingMaps/AssociateBy.kt
import buildingmaps.*
import atomictest.eq

fun main() {
  val map: Map<String, Person> =
    people().associateBy { it.name }
  map eq mapOf(
    "Alice" to Person("Alice", 21),
    "Arthricia" to Person("Arthricia", 15),
    "Bob" to Person("Bob", 25),
    "Bill" to Person("Bill", 25),
    "Birdperson" to Person("Birdperson", 42),
    "Charlie" to Person("Charlie", 21),
    "Crocubot" to Person("Crocubot", 42),
    "Franz" to Person("Franz", 21),
    "Revolio" to Person("Revolio", 33))
}

associateBy() must be used with a unique selection key and returns a Map that pairs each unique key to the single element selected by that key.

// BuildingMaps/AssociateByUnique.kt
import buildingmaps.*
import atomictest.eq

fun main() {
  // associateBy() fails when the key isn't
  // unique -- values disappear:
  val ages = people().associateBy { it.age }
  ages eq mapOf(
    21 to Person("Franz", 21),
    15 to Person("Arthricia", 15),
    25 to Person("Bill", 25),
    42 to Person("Crocubot", 42),
    33 to Person("Revolio", 33))
}

If multiple values are selected by the predicate, as in ages, only the last one appears in the generated Map.

getOrElse() tries to look up a value in a Map. Its associated lambda computes a default value when a key is not present. Because it’s a lambda, we compute the default key only when necessary:

// BuildingMaps/GetOrPut.kt
import atomictest.eq

fun main() {
  val map = mapOf(1 to "one", 2 to "two")

  map.getOrElse(0) { "zero" } eq "zero"

  val mutableMap = map.toMutableMap()
  mutableMap.getOrPut(0) { "zero" } eq
    "zero"
  mutableMap eq "{1=one, 2=two, 0=zero}"
}

getOrPut() works on a MutableMap. If a key is present it simply returns the associated value. If the key isn’t found, it computes the value, puts it into the map and returns that value.

Many Map operations duplicate ones in List. For example, you can filter() or map() the contents of a Map. You can filter keys and values separately:

// BuildingMaps/FilterMap.kt
import atomictest.eq

fun main() {
  val map = mapOf(1 to "one",
    2 to "two", 3 to "three", 4 to "four")

  map.filterKeys { it % 2 == 1 } eq
    "{1=one, 3=three}"

  map.filterValues { it.contains('o') } eq
    "{1=one, 2=two, 4=four}"

  map.filter { entry ->
    entry.key % 2 == 1 &&
      entry.value.contains('o')
  } eq "{1=one}"
}

All three functions filter(), filterKeys() and filterValues() produce a new map containing only the elements that satisfy the predicate. filterKeys() applies its predicate to the keys, and filterValues() applies its predicate to the values.

# Applying Operations to Maps

To map() a Map sounds like a tautology, like saying “salt is salty.” The word map represents two distinct ideas:

  • Transforming a collection
  • The key-value data structure

In many programming languages, the word map is used for both concepts. For clarity, we say transform a map when applying map() to a Map.

Here we demonstrate map(), mapKeys() and mapValues():

// BuildingMaps/TransformingMap.kt
import atomictest.eq

fun main() {
  val even = mapOf(2 to "two", 4 to "four")
  even.map {                            // [1]
    "${it.key}=${it.value}"
  } eq listOf("2=two", "4=four")

  even.map { (key, value) ->            // [2]
    "$key=$value"
  } eq listOf("2=two", "4=four")

  even.mapKeys { (num, _) -> -num }     // [3]
    .mapValues { (_, str) -> "minus $str" } eq
    mapOf(-2 to "minus two",
      -4 to "minus four")

  even.map { (key, value) ->
    -key to "minus $value"
  }.toMap() eq mapOf(-2 to "minus two", // [4]
    -4 to "minus four")
}
  • [1] Here, map() takes a predicate with a Map.Entry argument. We access its contents as it.key and it.value.
  • [2] You can also use a [destructuring declaration](javascript:void(0)) to place the entry contents into key and value.
  • [3] If a parameter isn’t used, an underscore (_) avoids compiler complaints. mapKeys() and mapValues() return a new map, with all keys or values transformed accordingly.
  • [4], map() returns a list of pairs, so to produce a Map we use the explicit conversion toMap().

Functions like any() and all() can also be applied to Maps:

// BuildingMaps/SimilarOperation.kt
import atomictest.eq

fun main() {
  val map = mapOf(1 to "one",
    -2 to "minus two")
  map.any { (key, _) -> key < 0 } eq true
  map.all { (key, _) -> key < 0 } eq false
  map.maxByOrNull { it.key }?.value eq "one"
}

any() checks whether any of the entries in a Map satisfy the given predicate, while all() is true only if all entries in the Map satisfy the predicate.

maxByOrNull() finds the maximum entry based on the given criteria. There may not be a maximum entry, so the result is nullable.

Exercises and solutions can be found at www.AtomicKotlin.com.