What a map is
A map stores key-value pairs and finds a value by its key. Looking a key up, adding a pair, changing a value and removing a pair all take constant time on average, which makes maps the standard tool for counting, grouping and remembering what you have already seen.
Scala has two families of maps. The default Map is immutable, and scala.collection.mutable.Map can be changed in place. Both are written with key -> value pairs, where -> builds a tuple of the key and the value.
Immutable maps
val scores = Map("Ana" -> 90, "Ben" -> 75)
println(scores.contains("Ana")) // true
println(scores("Ana")) // 90
println(scores.get("Ana")) // Some(90)
println(scores.get("Cy")) // None
println(scores.getOrElse("Cy", 0)) // 0
The ways of reading a key behave differently:
map(key)returns the value, and throws aNoSuchElementExceptionwith the messagekey not found: Cywhen the key is missing.map.get(key)returns anOption:Some(value)orNone.map.getOrElse(key, default)returns the value or the default you supply.map.contains(key)returns aBoolean.
An immutable map is never modified. An operation such as + returns a new map and leaves the original untouched:
val more = scores + ("Cy" -> 60)
println(scores.size) // 2
println(more.size) // 3
Mutable maps
When you want to update a map as the program runs, which is typical for counting, use the mutable one:
val counts = scala.collection.mutable.Map[String, Int]()
counts("x") = counts.getOrElse("x", 0) + 1
println(counts) // HashMap(x -> 1)
Writing counts("x") = value adds the key or replaces its value. Reading counts("x") for a key that isn't there throws, which is why the update above reads with getOrElse and a default of 0. The val only fixes which map the name refers to; the contents can still change.
The common operations on a mutable map:
m(key) = valueadds or replaces a pair.m += (key -> value)does the same with a tuple.m.remove(key)deletes the pair and returns anOptionwith the old value:Some(5)if the key was there,Noneotherwise.m.getOrElseUpdate(key, default)returns the value, and stores the default first if the key was missing.m.keysandm.valuesgive the keys and the values.
The exercise, step by step
The lesson tracks how many fruits of each kind there are.
- Create the map. Declare
fruitsas ascala.collection.mutable.Map[String, Int]holding"apples" -> 3,"bananas" -> 5and"grapes" -> 2. - Add four apples. Read the current count with
getOrElse("apples", 0), add 4 and store the sum back with index assignment. - Check for pears.
containstells whether a key exists. Negate it with!and printOut of pearswhen the key is absent. - Remove the bananas with
remove("bananas"). - Report every fruit. Loop over
fruits.keyswith the variablefruitand print an interpolated string that reads the count with${fruits(fruit)}.
val fruits: scala.collection.mutable.Map[String, Int] = scala.collection.mutable.Map(
"apples" -> 3,
"bananas" -> 5,
"grapes" -> 2
)
fruits("apples") = fruits.getOrElse("apples", 0) + 4
if (!fruits.contains("pears")) {
println("Out of pears")
}
fruits.remove("bananas")
for (fruit <- fruits.keys) {
println(s"We have ${fruits(fruit)} $fruit.")
}
The program prints:
Out of pears
We have 7 apples.
We have 2 grapes.
A few details are worth noting. The type annotation on fruits is optional here, because Scala infers the same type from the right-hand side. In the string s"We have ${fruits(fruit)} $fruit.", the s in front turns on interpolation: $fruit inserts a variable, and ${...} evaluates a whole expression. Finally, fruits.remove("bananas") returns Some(5), which the program simply ignores.
Common mistakes
- Relying on iteration order. A hash map doesn't promise any particular order for its keys. If the order matters, sort the keys first:
fruits.keys.toList.sorted. - Calling
map(key)on a missing key. It throws. Useget,getOrElseorcontainswhen the key might be absent. - Expecting an immutable map to change.
scores + ("Cy" -> 60)builds a new map. Using the original afterwards still shows two entries. - Forgetting the full name. The plain name
Mapis the immutable one. The mutable one needsscala.collection.mutable.Map, or an import.
Key takeaways
- A map finds a value by key in constant time on average.
Mapis immutable by default;scala.collection.mutable.Mapchanges in place.map(key)throws for a missing key, whilegetreturns anOptionandgetOrElsereturns a default.m(key) = valueadds or replaces, andremovedeletes.- Don't depend on the order in which keys come back.
