I'm trying to implement a lazy-construction / memoized evaluation / caching idiom in Rust.
There's an outer type which has a bunch of data and an accessor method. The accessor method needs to either return a cached calculation (if it has one) or calculate it and store the return value in a map for later use. The cached value doesn't need to reference the outer value so there's no circular reference problem; but it does need access to the outer value's data in order to construct itself.
Here's a full example which doesn't pass Rust's borrow checker:
use std::collections::HashMap;
pub struct ContainedThing {
count: usize,
}
impl ContainedThing {
fn create(thing: &Thing) -> ContainedThing {
// create uses an arbitrary number of attributes from Thing
// it doesn't keep any references after returning though
let count = thing.map.len();
ContainedThing { count: count }
}
}
struct Thing {
map: HashMap<i32, ContainedThing>,
}
impl Thing {
pub fn get(&mut self, key: i32) -> &ContainedThing {
self.map
.entry(key)
.or_insert_with(|| ContainedThing::create(&self))
}
}
fn main() {}
The specific error is:
error[E0502]: cannot borrow `self` as immutable because `self.map` is also borrowed as mutable
--> src/main.rs:24:29
|
22 | self.map
| -------- mutable borrow occurs here
23 | .entry(key)
24 | .or_insert_with(|| ContainedThing::create(&self))
| ^^ ---- borrow occurs due to use of `self` in closure
| |
| immutable borrow occurs here
25 | }
| - mutable borrow ends here
I'm having a really hard time figuring out a good way to implement this idiom. I tried pattern matching the return value of get()
instead of the entry()
API, but still fell foul of the borrow checker: the match
expression also ends up borrowing self
.
I can rewrite get
like this:
pub fn get(&mut self, key: i32) -> &ContainedThing {
if !self.map.contains_key(&key) {
let thing = ContainedThing::create(&self);
self.map.insert(key, thing);
}
self.map.get(&key).unwrap()
}
but this is pretty ugly (look at that unwrap
) and seems to require more lookups and copies than ought to be necessary. Ideally, I would like
- to only pay the cost of finding the hash entry once.
entry()
, done right, ought to track the insertion location when not found. - reduce the number of copies of the freshly constructed value. This may be infeasible, ideally I'd have an in-place construction.
- to avoid using
unwrap
; without a pointless pattern match, that is.
Is my clumsy code the best that can be achieved?