Can I do this using the Entry API?

I've read about the Entry API for hash maps, but I'm a bit stuck on how to use it. Consider the following:

// does not compile...

use std::collections::HashMap;

struct Item {
    id: u32,
    value: String,
}

struct Obj {
    map: HashMap<String, Item>,
}

impl Obj {
    pub fn check(&mut self, key: &str, check_id: u32) {
        // retrieve current item for key (if it exists)
        let item = self.map.get(key);
        // do some complex matching on the item to determine
        // what to do next
        match item {
            Some(&Item { id, .. }) if id == check_id => {
                // item exists, and has same id, nothing more to do
                println!("found checked id");
            },
            _ => {
                // else, we need to mutate the map, e.g. like this
                self.map.insert(String::from(key),
                                Item { id: check_id, value: String::new() });
            },
        }
    }
}

fn main() {
    let mut obj = Obj { map: HashMap::new() };
    // fill like...
    //obj.map.insert(String::from("a"), Item { id: 0, value: String::from("xxx") });
    obj.check("a", 0);
}

Obviously this does not compile since the item retrieved from map.get() keeps an immutable borrow of the map, so that there can't be an immutable borrow from insert.

Currently I see five ways to get around this:

  • do the retrieval and matching in a separate scope/function, which returns a flag indicating if the insert or other mutation has to be done
  • make Item cloneable and do self.map.get(key).map(|v| v.clone()) (btw, is there a better way to write this?) -- seems fine right now, but probably not anymore if the Item contains lots of data
  • remove the Item with remove instead of get, then reinsert it if necessary

This is what the entry API was written for, right? How to do it?

Yes, absolutely! This should have the same effect as your code:

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

struct Item {
    id: u32,
    value: String,
}

struct Obj {
    map: HashMap<String, Item>,
}

impl Obj {
    pub fn check(&mut self, key: &str, check_id: u32) {
        // retrieve current item for key (if it exists)
        let item = self.map.entry(String::from(key));
        // do some complex matching on the item to determine
        // what to do next
        match item {
            Entry::Occupied(ref entry) if entry.get().id == check_id => {
                // item exists, and has same id, nothing more to do
                println!("found checked id");
            },
            Entry::Occupied(mut entry) => {
                // item exists, but with an other id, so replace it with a new one
                *entry.get_mut() = Item { id: check_id, value: String::new() };
            },
            Entry::Vacant(entry) => {
                // else, we need to mutate the map, e.g. like this
                entry.insert(Item { id: check_id, value: String::new() });
            },
        }
    }
}

fn main() {
    let mut obj = Obj { map: HashMap::new() };
    // fill like...
    //obj.map.insert(String::from("a"), Item { id: 0, value: String::from("xxx") });
    obj.check("a", 0);
}

I'm not sure if you want to replace the item if it has a different id, but you can tweak it to your liking :smile:

2 Likes

Thanks, that helped a lot! I feel like such an example is missing from the docs (they only demonstrate or_insert on the main entry docs, and there are no further examples).

1 Like

More examples can be found at

Yes, but as I said, they don't show off matching the Entry - only using or_insert() (which might be the common case).

At least for me it was not clear how to use the Vacant and Occupied [shouldn't it be Engaged :slight_smile: ] items to match.

Err whoops, I forgot I updated them to use or_insert, and then couldn't think of a complicated enough scenario to merit the more advanced usage (that was short).

Fair enough! I admit that my snippet wouldn't be a good example either :wink: