HandsomeGorilla
HandsomeGorilla

Reputation: 625

Why are the values in a given HashMap mutable when I don't believe I have explicitly declared them as such?

I wrote a working solution to the third proposed exercise in section 8.3 of The Book, but some of the behavior defies my intuition. Specifically, it appears that I'm able to mutate a vector that appears to be instantiated as immutable.

I've included the portions of the code that I believe are relevant, eliding the portions of code that do not interact with the Vecs stored in the HashMap.

I've engaged in some speculation after the code block, but I could really use a more sure-footed explanation of what's actually happening.

// Start by declaring a new HashMap. This is where the mystery begins for me.
let mut departments: HashMap<String, Vec<String>> = HashMap::new();

// This code deals with getting user input from stdin and has been elided
// ...

// Match the first string slice
match args[0] {
  
  // Add an employee to a department in the HashMap
  "add" => {
    
    // POINT OF INTEREST 1
    // Adding a department that isn't already in the HashMap will cause a new empty
    // vector to be inserted at the newly created key
    let list = departments.entry(args[2].to_string()).or_insert(Vec::new());

    // In any case, we insert the employee's name into the vector associated with
    // whatever department we got from stdin
    list.push(args[1].to_string());
  }

  // List employees
  "who" => match args[1] {

    // List all employees in every department, employees shown in alphabetical order
    "all" => {
      for (department, employees) in &mut departments {

        // POINT OF INTEREST 2
        // Why am I allowed to sort this vector? The artifact underlying the reference
        // was never explicitly made mutable.
        employees.sort();

        for ee in employees {
          println!("{}: {}", department, ee);
        }
      }
    }

    // List all employees in a given department, employees shown in alphabetical order
    dept => {
      let employees = departments.get_mut(dept);

      match employees {
        Some(entries) => {

          // POINT OF INTEREST 3
          // This one is seems the least mysterious to me, since I get the value
          // of the HashMap at `dept` through `.get_mut()`.
          println!("{}:", dept);
          entries.sort();

          for ee in entries {
            println!("\t{}", ee);
          }
        }
        _ => (),
      }
    }
  }
}

Hypothesis 1: At POINT OF INTEREST 1, my call to .or_insert() returns a mutable reference to a new vector, and this is why later calls to .sort() on values in the HashMap work.

This does not seem like the likely answer! In the beginning, I declared departments to be of the type HashMap<String, Vec<String>>, not HashMap<String, &mut Vec<String>>.

Hypothesis 2: When I declare departments as mutable, its keys and values inherit that mutability. This also seems unlikely, as nothing in my (very limited) experience has suggested such a thing is a feature of Rust. I also like to think that if that were explicitly stated in the first 8 chapters of The Book it would have caught my attention, but I've been known to skim over important details before.

Upvotes: 3

Views: 638

Answers (1)

John Kugelman
John Kugelman

Reputation: 361605

for (department, employees) in &mut departments {

The for loop leverages this IntoIter implementation:

impl<'a, K, V, S> IntoIterator for &'a mut HashMap<K, V, S> {
    type Item = (&'a K, &'a mut V);
}

Because of this implementation, when you iterate over a &mut HashMap<K, V> you get back tuples of (&K, &mut V). Notice that the keys are borrowed immutably while the values are mutable. This lets you modify the values, because employees is of type &mut Vec<String>.

Why is the map able to return mutable references? The map owns both the keys and values, so it can return mutable references to either of them if it wants to. That's what being an owner means: you can let others mutably borrow your objects if you so wish.

HashMap is happy to let you mutate values because that won't affect the data structure. It doesn't let you modify the keys because that would change their hashes and invalidate where they're stored in the hash table. HashMap could return &mut K. The borrow checker wouldn't stop it. It doesn't, though, because then callers could corrupt the hash map.

Upvotes: 5

Related Questions