Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Want to add to HashMap using pattern match, get borrow mutable more than once at a time

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.

like image 660
cflewis Avatar asked Jun 15 '15 17:06

cflewis


2 Answers

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

like image 169
eulerdisk Avatar answered Oct 12 '22 03:10

eulerdisk


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.

like image 38
A.B. Avatar answered Oct 12 '22 01:10

A.B.