# Building Maps
Map
s 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 List
s 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 aMap.Entry
argument. We access its contents asit.key
andit.value
. - [2] You can also use a [destructuring declaration](javascript:void(0)) to place the entry contents into
key
andvalue
. - [3] If a parameter isn’t used, an underscore (
_
) avoids compiler complaints.mapKeys()
andmapValues()
return a new map, with all keys or values transformed accordingly. - [4],
map()
returns a list of pairs, so to produce aMap
we use the explicit conversiontoMap()
.
Functions like any()
and all()
can also be applied to Map
s:
// 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.