Scala from the ground up

Maps and sets

A Map for lookups by key and a Set for values that appear once, with get, getOrElse and updated, and a sorted print so the output is the same every run.

Part 9 of 12

Advanced17 min readBack to Courses

A Map is a lookup table

Map("ada" -> 36, "bob" -> 41) holds pairs, and the arrow is how one is written. Reading with round brackets — ages("ada") — gives the value, but that form throws when the key is missing, so the safe questions are get, which answers an Option, and getOrElse, which hands back a plain value with your fallback in it.

A Map cannot hold the same key twice; writing a key again replaces what was there. updated is the method that builds the changed copy: ages.updated("ada", 37) returns a new map and leaves ages alone, so a map held in a val is still safe to pass to anything.

Keys, and a printing order you can trust

keys gives the keys and values gives the values, while size counts the pairs. What a map does not promise is an order: printing it directly can look scrambled, and another version of the library may scramble it differently. The values are reliable; the sequence they arrive in is not.

The repair is to sort before printing. ages.toSeq.sortBy(p => p._1) turns the map into a sequence of pairs ordered by key, so the output reads the same on every run and can be compared between runs. _1 and _2 are how the two parts of a pair are named, and a pair is just a tuple of two.

A Set keeps one of each

Set("red", "blue", "red") has two members, because a set has no duplicates and no order of its own. contains answers whether a value is in it, and adding with + returns a new set rather than changing the old one, exactly as updated did for the map.

++ joins two collections, and joining two sets gives a set, so anything that appeared in both collapses into one member. Print a set through toList.sorted for the same reason a map is sorted: a sorted line is one you can check by eye, and the sorted form is stable between runs.

How the lookup finds a key

A Map is a hash table. It turns a key into a number with hashCode, uses that number to choose a bucket, and then compares candidates with equals. That is why a key needs both to be stable: a string or a case class works because its value decides the hash, while a mutable object used as a key can move to a different bucket and become unfindable.

The bracket form ages("ada") is the apply method, and it throws when the key is absent. get returns an Option instead, and getOrElse turns that option into a plain value with a fallback. Both of the safe forms leave the decision to the caller rather than stopping the program on a missing key.

A Set is a Map with the values dropped: it cares only whether a member is present, not what is attached to it. Uniqueness is decided by the same equals and hashCode, so adding a duplicate changes nothing. Both collections are immutable, so updated, + and ++ return new collections and leave the originals usable.

A map is often built from a list

A map is often built from a list rather than typed out. words.groupBy(w => w.length) gives a map from length to the words of that length, and nums.zip(names).toMap pairs two lists into a lookup table. Both return a new map and leave the inputs alone, like every other collection method here.

When the same key appears twice, the last pair wins. That is silent, and it is the reason a count is usually built with groupBy and a map over the groups rather than by adding pairs one at a time.

Three map mistakes: assuming the pairs print in the order they were written, listing a key twice in one Map(...) (the last value wins silently), and using a mutable object as a key so that a later change makes it unfindable. Sort before printing, check the literal when a count looks wrong, and keep keys immutable.

Try these next

  • Build a map of three countries to their capitals and print it sorted by country.
  • Count how many times each letter appears in a short word using foldLeft and updated.
  • Add the same value to a set twice and print its size before and after.

Lookups, a change, and a sorted print

val ages = Map("ada" -> 36, "bob" -> 41)

println(ages("ada"))               // 36
println(ages.get("ada"))           // Some(36)
println(ages.get("zed"))           // None
println(ages.getOrElse("zed", 0))  // 0

val older = ages.updated("ada", 37)
println(older("ada"))              // 37
println(ages("ada"))               // 36

// Sort the pairs so the order is fixed.
for (pair <- ages.toSeq.sortBy(p => p._1)) {
  println(pair._1 + " -> " + pair._2)
}

val tags = Set("red", "blue", "red")
println(tags.size)                 // 2
println(tags.contains("red"))      // true
println((tags + "green").toList.sorted)

The price list as a map

Run it and read the two lookups that fail without an error. Then add a fourth person, print the map count, and see what `updated` leaves behind in the original.

Output
Click "Run" to execute your code

Counting words with a map

Run it and check each count by hand. Then add two more words to the list, and use `getOrElse` to ask for a word that is not there.

Output
Click "Run" to execute your code

ages("someKey") throws when the key is not there, and the program stops on that line. Use getOrElse for a fallback and get when the caller should decide what happens — neither of those ever throws.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does ages.get("zed") give when zed is not a key?