22

I am trying to write some toy code that stores the number of times it sees a word in a HashMap. If the key exists, it increments a counter by one, if the key doesn't exist, it adds it with the value 1. I instinctively want to do this with a pattern match, but I hit a borrow mutable more than once error:

fn read_file(name: &str) -> io::Result<HashMap<String, i32>> {
    let b = BufReader::new(File::open(name)?);
    let mut c = HashMap::new();

    for line in b.lines() {
        let line = line?;
        for word in line.split(" ") {
            match c.get_mut(word) {
                Some(i) => {
                    *i += 1;
                },
                None => {
                    c.insert(word.to_string(), 1);
                }
            }
        }
    }

    Ok(c)
}

The error I get is:

error[E0499]: cannot borrow `c` as mutable more than once at a time
  --> <anon>:21:21
   |
16 |             match c.get_mut(word) {
   |                   - first mutable borrow occurs here
...
21 |                     c.insert(word.to_string(), 1);
   |                     ^ second mutable borrow occurs here
22 |                 }
23 |             }
   |             - first borrow ends here

I understand why the compiler is grumpy: I've told it I'm going to mutate the value keyed on word, but then the insert isn't on that value. However, the insert is on a None, so I would have thought the compiler might have realized there was no chance of mutating c[s] now.

I feel like this method should work, but I am missing a trick. What am I doing wrong?

EDIT: I realize I can do this using

        if c.contains_key(word) {
            if let Some(i) = c.get_mut(s) {
                *i += 1;
            }
        } else {
            c.insert(word.to_string(), 1);
        }

but this seems horribly ugly code vs the pattern match (particularly having to do the contains_key() check as an if, and then essentially doing that check again using Some.

Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
cflewis
  • 7,251
  • 7
  • 28
  • 37

3 Answers3

18

You have to use the Entry "pattern":

use std::collections::HashMap;
use std::collections::hash_map::Entry::{Occupied, Vacant};

fn main() {
    let mut words = vec!["word1".to_string(), "word2".to_string(), "word1".to_string(), "word3".to_string()];
    let mut wordCount = HashMap::<String, u32>::new();

    for w in words {
        let val = match wordCount.entry(w) {
           Vacant(entry) => entry.insert(0),
           Occupied(entry) => entry.into_mut(),
        };

        // do stuff with the value
        *val += 1;
    }

    for k in wordCount.iter() {
        println!("{:?}", k);
    }
}

The Entry object allows you to insert a value if its missing, or to modify it if it already exists.

https://doc.rust-lang.org/stable/std/collections/hash_map/enum.Entry.html

eulerdisk
  • 4,299
  • 1
  • 22
  • 21
  • 2
    Note that `match`ing manually is almost never necessary. The methods `or_insert()` and `or_insert_with()` offer a more concise way to achieve this. See A.B.'s answer for information on those. – Lukas Kalbertodt Apr 02 '17 at 12:52
16

HashMap::entry() is the method to use here. In most cases you want to use with Entry::or_insert() to insert a value:

for word in line.split(" ") {
    *c.entry(word).or_insert(0) += 1;
}

In case the value to be inserted need to be expensively calculated, you can use Entry::or_insert_with() to make sure the computation is only executed when it needs to. Both or_insert methods will probably cover all of your needs. But if you, for whatever reason, want to do something else, you can still simply match on the Entry enum.

Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
A.B.
  • 15,364
  • 3
  • 61
  • 64
4

This is basically not an issue anymore. With non-lexical lifetimes (NLL), your code compiles without problems. Your example on the Playground.

NLL is a new way the compiler reasons about borrows. NLL has been enabled in Rust 2018 (≥ 1.31). It is planned to be enabled in Rust 2015 eventually as well. You can read more about NLL and editions in this official blog post.

In this particular case, I still think A.B.'s answer (entry(word).or_insert(0)) is the best solution, simply because it is very concise.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305