(Im)mutable borrows and storing objects

I'm having an issue resolving an borrow issue. I think I understand the problem, but don't know how to resolve it.

I have a data store where I keep Things. If a Thing is loaded, I want to return a reference to it. If a Thing is not yet loaded, I want to load it, store it, and return a reference to the newly loaded Thing. This is where I run into the borrow issues because to store the thing I need a mutable borrow, but to return a reference I need an immutable borrow. However, because I return if the thing exists already , it feels like the first immutable borrow of self.store should not overlap with the mutable borrow of it.

I tried to scope the different chunks of the function but it didn't help.

What am I misunderstanding the problem? Is there a canonical way to solve this?

Here is a minimal code example to demonstrate the issue, but please let me know if more context is required.

fn load_thing(&mut self, path: &Path) -> &Thing {
  // check if already loaded
  if let Some(thing) = self.store.get_thing(&path) { // immutable borrow of `self.store`
    return Ok(thing);
  }

  // load thing
  let thing = Thing::load(&path)?;
  let _old_thing = self.store.insert_thing(thing)?; // mutable borrow of `self.store`

  // get newly inserted thing
  if let Some(thing) = self.store.get_thing(&path) { // immutable borrow of `self.store`
    return Ok(thing);
  }

  Err(Error("thing could not be loaded"))
}

This is exactly the same problem as this post. TL;DR: an easy fix is to put items behind an Rc and return a clone of it.

1 Like

Thanks @H2CO3 , so in practice the only option is to hide the stored objects behind a smart pointer-like object?

Another alternative is to use an append-only collection for store, like the ones from the elsa crate.

(NB: I have never used these myself, so can’t vouch for the implementation quality)

1 Like

The OP looks like conditional return of a borrow to me.

A workaround could be an Entry-like API where you only have to borrow self.store once, and can then do either existing entry extraction or new entry insertion.

If you're already using something with an Entry API like HashMap has, you could wrap that up in a Store::get_or_insert_with like method.

    fn get_or_insert<F>(&mut self, path: PathBuf, producer: F) -> Result<&Thing, ()>
    where
        F: FnOnce() -> Result<Thing, ()>
    {
        match self.hm.entry(path) {
            Entry::Vacant(ve) => {
                producer()
                    .map(|thing| &* ve.insert(thing))
            }
            Entry::Occupied(oe) => {
                Ok(&*oe.into_mut())
            }
        }
    }
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.