Can I do this using the Entry API?


#1

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?


#2

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:


#3

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).


#4

More examples can be found at
https://doc.rust-lang.org/std/collections/#entries


#5

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.


#6

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).


#7

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