rustrefcell

How to return an *optional* reference into RefCell contents


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.


Solution

  • 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:

    And of course, this is all moot if Ref::filter_map becomes stable.