Chapter 8

HashMaps

👋 Anyone can read and edit this exercise. Sign up to save your progress.

A HashMap<K, V> stores key-value pairs and lets you look up a value by its key in (on average) constant time. Nobody ever got fired for using it for caches, indexes, counters, configuration, and anything else where "given X, find Y" is the question.

Contrary to Vec<T>, HashMap is not in scope by default, so you have to import it first:

use std::collections::HashMap;

let mut config: HashMap<String, String> = HashMap::new();
config.insert("host".to_string(), "localhost".to_string());

let host = config.get("host"); // Option<&String>

Two things to notice:

A common pattern is "increment a counter for this key, default to 0":

let mut counts: HashMap<String, u32> = HashMap::new();
for word in ["a", "b", "a"] {
    *counts.entry(word.to_string()).or_insert(0) += 1;
}

entry().or_insert() is the idiomatic way to do "look up, or insert a default and then take a mutable reference to it" in one step.

A note on * (dereference)

The * in front of counts.entry(...).or_insert(0) is the dereference operator. or_insert(0) hands back a &mut u32 (a pointer to the value inside the map) and * reaches through that pointer so we can actually update the u32 it points at:

let mut n = 41;
let r: &mut i32 = &mut n;
*r += 1; // updates `n`, not `r`

Without the *, you'd be trying to add 1 to a reference, which the compiler won't let you do. References show up properly in chapter 11; for now it's enough to know that when a function returns &mut T, you reach the T through *.

Creating a config map

The simplest way to use a HashMap is to create one and insert a few key-value pairs. HashMap::new() gives you an empty map; the type is usually inferred from the first insert.

Here you'll build a small configuration map with two defaults: a "host" and a "port".

Useful from the standard library

  • HashMap::new creates an empty map. The map needs to be mut to insert into it.
  • HashMap::insert adds (or replaces) a key-value pair. Both arguments are owned, so &str literals need a .to_string() to become String.
Exercise 1 of 4
Open in Web Editor

Results

    Compiler / runtime output
    
                

    Setting a value

    Updating a HashMap is the same operation as adding to it: one method covers both cases, and it doesn't care whether the key was already there. If the key existed, the old value is replaced (and returned); if not, it's inserted fresh.

    Useful from the standard library

    • HashMap::insert is the only call you need here. Returns Option<V>: Some(old) if the key was already present, None otherwise. You can ignore the return value when you don't care.
    • HashMap::contains_key is a quick "is this key present?" check that doesn't retrieve the value. Not needed for this exercise, but worth knowing.
    Exercise 2 of 4
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  

      Getting a value with a fallback

      Looking up a key in a HashMap returns an Option<&V>, because the key might not be there. There's no null. To collapse the Option into a concrete value you supply a fallback for the missing case. We'll cover Option properly in chapter 9; here you only need one combinator.

      Useful from the standard library

      • HashMap::get returns Option<&V>. The & matters: you get a reference into the map, not a copy of the value.
      • Option::cloned turns Option<&String> into Option<String> so you can return an owned value.
      • Option::unwrap_or pulls a value out, falling back to the argument when the option is None. Reads as "use this value, or default to that one".
      Exercise 3 of 4
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    

        Counting with `entry`

        Counting occurrences is the canonical "look up; if missing, insert a default; then update" workflow. Doing it by hand with contains_key and get_mut works but does two lookups and fights the borrow checker. The entry API does it in one step.

        Useful from the standard library

        • HashMap::entry reserves the slot for a key in one lookup and hands you back an Entry you can operate on.
        • Entry::or_insert returns a &mut V: either to the existing value, or to the default it just inserted. The full pattern reads *map.entry(key).or_insert(0) += 1.
        • The * is the dereference operator from the chapter intro: it reaches through the &mut V so the += 1 updates the count inside the map.
        Exercise 4 of 4
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      

          Wrapping up hashmaps

          You built a configuration map from scratch, updated and read values, and used the entry API to write the canonical word counter in one line.

          What we learned

          • HashMap<K, V> stores key-value pairs and looks them up in (average) constant time. All keys share one type; all values share one type. Mix-and-match goes through enums.
          • HashMap lives in std::collections and isn't in the prelude, so you need a use std::collections::HashMap; at the top of the file.
          • insert does double duty: it adds a new pair or replaces an existing one, and returns the previous value (if any) as Option<V>.
          • get returns Option<&V>, never null. Combine with unwrap_or (or cloned().unwrap_or(...) when you need an owned value) to handle the missing case.
          • entry(key).or_insert(default) is the idiomatic "look up; if missing, insert a default; then return a &mut V" pattern. It does one lookup instead of two and sidesteps the borrow checker.
          • Reach through a &mut T with * to update the value it points at: *map.entry(k).or_insert(0) += 1. References get a proper treatment in chapter 11.
          Next chapter 9Tuples and Destructuring