I have a type that stores its data in a container behind a Rc<RefCell<>>
, which is for the most part hidden from the public API. For example:
struct Value;
struct Container {
storage: Rc<RefCell<HashMap<u32, Value>>>,
}
impl Container {
fn insert(&mut self, key: u32, value: Value) {
self.storage.borrow_mut().insert(key, value);
}
fn remove(&mut self, key: u32) -> Option<Value> {
self.storage.borrow_mut().remove(&key)
}
// ...
}
However, peeking inside the container requires returning a Ref
. That can be achieved using Ref::map()
- for example:
// peek value under key, panicking if not present
fn peek_assert(&self, key: u32) -> Ref<'_, Value> {
Ref::map(self.storage.borrow(), |storage| storage.get(&key).unwrap())
}
However, I'd like to have a non-panicking version of peek
, that would return Option<Ref<'_, Value>>
. This is a problem because Ref::map
requires that you return a reference to something that exists inside the RefCell
, so even if I wanted to return Ref<'_, Option<Value>>
, it wouldn't work because the option returned by storage.get()
is ephemeral.
Trying to use Ref::map
to create a Ref
from a previously looked up key doesn't compile either:
// doesn't compile apparently the borrow checker doesn't understand that `v`
// won't outlive `_storage`.
fn peek(&self, key: u32) -> Option<Ref<'_, Value>> {
let storage = self.storage.borrow();
if let Some(v) = storage.get(&key) {
Some(Ref::map(storage, |_storage| v))
} else {
None
}
}
The approach that does work is to perform the lookup twice, but that's something I'd really like to avoid:
// works, but does lookup 2x
fn peek(&self, key: u32) -> Option<Ref<'_, Value>> {
if self.storage.borrow().get(&key).is_some() {
Some(Ref::map(self.storage.borrow(), |storage| {
storage.get(&key).unwrap()
}))
} else {
None
}
}
Compilable example in the playground.
Related questions like this one assume that the inner reference is always available, so they don't have that issue.
I found Ref::filter_map()
which would solve this, but it's not yet available on stable, and it's unclear how far it is from stabilization. Barring other options, I would accept a solution that uses unsafe
provided it is sound and relies on documented guarantees.
You could use a side effect to communicate whether the lookup succeeded, then return an arbitrary value from Ref::map
if you don't have a successful value.
impl Container {
// ...
fn peek(&self, key: u32) -> Option<Ref<'_, Value>> {
let storage = self.storage.borrow();
if storage.is_empty() {
// The trick below requires the map to be nonempty, but if it's
// empty, then we don't need to do a lookup.
return None;
}
// Find either the correct value or an arbitrary one, and use a mutable
// side channel to remember which one it is.
let mut failed = false;
let ref_maybe_bogus: Ref<'_, Value> = Ref::map(storage, |storage| {
storage.get(&key).unwrap_or_else(|| {
// Report that the lookup failed.
failed = true;
// Return an arbitrary Value which will be ignored.
// The is_empty() check above ensured that one will exist.
storage.values().next().unwrap()
})
});
// Return the ref only if it's due to a successful lookup.
if failed {
None
} else {
Some(ref_maybe_bogus)
}
}
}
Refinements:
If the Value
type can have constant instances, then you can return one of those instead of requiring the map to be nonempty; the method above is just the most general one that works for any definition of Value
, and not the simplest. (This is possible since a &'static Value
satisfies Ref
's requirements — the reference just needs to live long enough, not to actually point into the RefCell
's contents.)
If the Value
type can have a constant instance that is distinct from any meaningful instance that would be found in the map (a "sentinel value"), then you can check for that value in the final if
instead of checking a separate boolean variable. However, this doesn't especially simplify the code; it's mostly useful if you have a sentinel you're using for other purposes anyway, or if you like a “pure functional” coding style which avoids side effects.
And of course, this is all moot if Ref::filter_map
becomes stable.