1
votes

I am using HashMap but I'm stumbling over how to "release" a mutable borrow of the HashMap and cannot find a good explanation of how to do this.

This is just an example and that the goal is not to "solve the problem" but rather to understand how to accomplish this and/or why it should not be done this way.

The example consists of a HashMap storing some simple Records:

type Map = HashMap<String, Record>;

pub struct Record {
    pub count: u32,
    pub name: String,
}

impl Record {
    fn new<S: Into<String>>(name: S) -> Record {
        Record { name: name.into(), count: 0 }
    }

    pub fn add<'a>(&'a mut self, inc: u32) -> &'a mut Record {
        self.count += inc;
        self
     }
 }

The add function is there to have a mutable function on the record but is not the real culprit here.

We now want to implement a function that returns a reference to a Record in the HashMap so that we can modify it in-place. In addition to this, we want to be able to have control over the returned reference so that we can do some side effects (for this example it is sufficient that we assume that we want to print out what is happening, but it could be some other action to handle statistics and/or accessing some other storage or do lazy evaluation). To handle this, we introduce a Handle struct that keeps a reference to the Record as well as a reference to the HashMap that the record is coming from.

pub struct Handle<'a> {
    map: &'a Map,
    record: &'a Record,
}

impl<'a> Handle<'a> {
    fn new(record: &'a Record, map: &'a Map) -> Handle<'a> {
         println!("Retrieving record");
         Handle { record: record, map: map }
    }

    fn mut_record(&mut self) -> &mut Record {
        println!("Modifying record");
        self.record
    }
}

Let's assume that we need both references for some reason and note that we are fine with retaining an immutable borrow to the HashMap while the handle is in existence, so no modifications of the HashMap should happen.

The Handle is just temporary and we expect that it can be used roughly like this:

let mut map = HashMap::new();
let foo = get_or_insert(&mut map, "foo");
foo.mut_record().do_something(|record| record.add(3))

The first implementation of get_or_insert is this:

pub fn get_or_insert<'a, S>(map: &'a mut Map, name: S) -> Handle<'a>
    where S: Into<String>
{
    let key = name.into();
    let record = map.entry(key.clone()).or_insert(Record::new(key));
    Handle::new(record, map)
}

This gives the following error:

error[E0502]: cannot borrow `*map` as immutable because it is also borrowed as mutable
  --> hashmap.rs:65:29
   |
64 |         let record = map.entry(key.clone()).or_insert(Record::new(key));
   |                      --- mutable borrow occurs here
65 |         Handle::new(record, map)
   |                             ^^^ immutable borrow occurs here
66 |     }
   |     - mutable borrow ends here

There are two references to the HashMap with the first one being a mutable borrow. We need to "release" the first mutable borrow of the map before we can grab an immutable borrow. I tried to write the code in this manner and added a scope around the first mutable borrow expecting it to be "released" when the scope ends:

pub fn get_or_insert<'a, S>(map: &'a mut Map, name: S) -> Handle<'a>
    where S: Into<String>
{
    let key = name.into();
    let record = {
        map.entry(key.clone()).or_insert(Record::new(key))
    };
    Handle::new(record, map)
}

But the error remains.

It is very strange that the mutable borrow remain even after the scope has finished. According to References and Borrowing the borrow should end at the end of the scope, and according to Scope and shadowing scopes are controlled by blocks, which are collections of statements enclosed in braces, so superficially, it appears that the later function definition should end the scope with the mutable borrow of the reference to the map.

How do you implement a Handle like this in reasonable way so that the lifetime of Handle does not exceed that of the HashMap and capture this at compile time? I'm looking at a good way to create a Handle that

  • Abstract away the access to the underlying storage by using the temporary Handle as the abstraction provided by the implementation.

  • Capture misuses at compile time rather than runtime, which disqualifies RefCell and Rc.

  • Perform a single lookup in the underlying structure.

I looked at RefCell but that moves the checks from compile time to runtime, and it would be beneficial to be able to catch misuses of Handle at compile time.

The question in Rust: Borrowing issues with attempted caching is similar to this one but the answer is using UnsafeCell, which work around the checks rather than solve them.

To me the problem seems to be that there need to be a way to transform a mutable reference to a immutable reference and release the mutable borrow (with the restriction that it should be permitted by the code), but still not sure if I have misunderstood something.

Update: There were originally three bullets for this question in an attempt to make it more structured, but it's been rewritten to just pose a single question to make it clear what the goal is.

1
I'm not sure of the end goal here. But you should be able to create a wrapper object and wrap the value returned by get_mut()creativcoder
Well... this is, sort of, what I'm trying to do (Handler is the wrapper object). Problem is again how to release the mutable borrow once you've used it to locate the Record that you want to wrap.Mats Kindahl
Sorry, but I do not really follow how the question you reference provide an answer. The mutable borrow is wrapped in a block (that return a mutable reference to a record) but it still does not end the mutable borrow (of the map).Mats Kindahl
Also, it is hard to split this example up any further since the problem is really about lifetimes. The real question is the last bullet, the first two are just extracting parts of the problem, but I can rewrite the post so that it contain a single question.Mats Kindahl
Rewrote the text and the title. See if you think it is clearer.Mats Kindahl

1 Answers

1
votes

On this line:

let record = map.entry(key.clone()).or_insert(Record::new(key));

record is of type &'a mut Record, because or_insert returns a mutable reference to the value stored in the HashMap. This keeps the borrow on map active; that's why you get the error.

One solution is to lookup the value using get after the insertion in order to obtain an immutable borrow.

pub fn get_or_insert<'a, S>(map: &'a mut Map, name: S) -> Handle<'a>
    where S: Into<String>
{
    let key = name.into();
    map.entry(key.clone()).or_insert(Record::new(key.clone()));
    let record = map.get(&key).unwrap();
    Handle::new(record, map)
}

Note that this still doesn't let you implement Handle::mut_record with the signature you gave; Handle only has immutable references to the map and to the record, and you can't obtain a mutable reference to a record with those.