Return Option from Ref::map

I store a container inside a RefCell. And I want to return a reference to an element from the method. But the element may be not present, so I need to return Option. I managed to write such a function, but it does not look good, at least it double-checks for the existence of the element and the second check is unwrap().

Can it be improved?

Also, I do not like that I can't just debug print the result, but that's a side question.

playground

use std::cell::Ref;
use std::cell::RefCell;
use std::collections::HashMap;
use std::ops::Deref;

#[derive(Default)]
struct MyMap {
    map: RefCell<HashMap<u32, String>>,
}

impl MyMap {
    fn get(&self, key: u32) -> Option<impl Deref<Target = String> + '_> {
        let map = self.map.borrow();
        if map.contains_key(&key) {
            Some(Ref::map(self.map.borrow(), |map| map.get(&key).unwrap()))
        } else {
            None
        }
    }
    
    fn insert(&self, key: u32, value: String) {
        self.map.borrow_mut().insert(key, value);
    }
}

fn main() {
    let map = MyMap::default();
    println!("get(0): {:?}", map.get(0).is_some());
    map.insert(0, "aaa".to_string());
    println!("get(0): {:?}", *map.get(0).unwrap());
}

The answer to the original question is filter_map.

    fn get(&self, key: u32) -> Option<impl Deref<Target = String> + '_> {
        Ref::filter_map(self.map.borrow(), |map| map.get(&key)).ok()
    }

But I have new question. How can I return an iterator?

You return iterators in exactly the same way you would return values of any other type. What precisely are you trying to do?

The returned iterator would borrow from and outlive the temporary Ref returned by RefCell::borrow()

Can you share some specific, actual code that exhibits the error?

The obvious approach fails (returned object borrows from temporary):

fn iter(&self) -> impl Iterator<Item = (&u32, &String)> {
    self.map.borrow().iter()
}

Ref::map also fails since it can only wrap a reference:

fn iter(&self) -> RefCell<std::collections::hash_map::Iter<u32, String>> {
    Ref::map(self.map.borrow(), |m| m.iter())
}

The core problem is that the returned iterator needs to hold:

  • Ref
  • A hash_map::Iter

The inner iterator needs to borrow from Ref, which isn't normally allowed. A handy exemption from that is a pinned async block. genawaiter wraps pinned async blocks and gives them an Iterator interface.

fn iter(&self) -> impl Iterator<Item = (u32, String)> + '_ {
    let map = self.map.borrow();
    genawaiter::rc::Gen::new(|co| async move {
        let it = map.iter();
        for (k, v) in it {
            co.yield_((*k, v.clone())).await;
        }
    })
    .into_iter()
}
fn main() {
    let map = MyMap::default();
    println!("get(0): {:?}", map.get(0).is_some());
    map.insert(0, "aaa".to_string());
    map.insert(10, "www".to_string());
    println!("get(0): {:?}", *map.get(0).unwrap());
    for i in map.iter() {
        println!("iter: {:?}", i);
    }
}

Unfortunately I had to clone the returned string.

I was thinking of storing the Ref and the iterator together so that the iterator references the data that lives long enough. But that means creating a self-referential data structure, which is not easy in Rust also.

Another option: newtype the Ref and impl IntoIterator on references to it.

impl MyMap {
    fn borrow(&self) -> MapRef {
        MapRef(self.map.borrow())
    }
}

struct MapRef<'a>(Ref<'a, HashMap<u32, String>>);

impl<'a> IntoIterator for &'a MapRef<'a> {
    type Item = (&'a u32, &'a String);
    type IntoIter = std::collections::hash_map::Iter<'a, u32, String>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.iter()
    }
}

fn main() {
    let map = MyMap::default();
    println!("get(0): {:?}", map.get(0).is_some());
    map.insert(0, "aaa".to_string());
    map.insert(10, "www".to_string());
    println!("get(0): {:?}", *map.get(0).unwrap());
    for i in &map.borrow() {
        println!("iter: {:?}", i);
    }
    map.insert(20, "xxx".to_string());
    println!();
    for i in &map.borrow() {
        println!("iter: {:?}", i);
    }
}
3 Likes

Heh, this is what I was going to write up.

The only real reason not to return the Ref<'_, _> at that point is because you want to hide the implementation detail of that type. If you don't care about hiding the type of the HashMap in addition, you could just implement Deref.

RefCell/Ref is basically a single-thread runtime-checked Mutex/MutexGuard.[1] So this is basically going over why &Mutex<HashMap<_, _>> doesn't implement IntoIterator and so on: there's no way to pass back an intermediate lock that stays alive long enough. (And items have to be able to outlast the Iterator that doled them out, which is presumably why you had to clone in your generator.)


  1. More like RwLock but most people are more familiar with locking and releasing Mutex. â†Šī¸Ž

2 Likes