Transposition and RefCell

I am looking for better ways of dealing with Ref-wrapped values. I have an application that is recursively collecting references from vectors. Below is a simplified non-recursive version of it. Do you see ways to improve it? Should std::cell::Ref have a filter_map function?

use std::cell::{Ref, RefCell};

fn main() {
    let strings = RefCell::new(vec![Some("one".to_string()), None, Some("two".to_string())]);

    // Given.
    let borrowed_vec: Ref<Vec<Option<String>>> = strings.borrow();

    // Objective.
    let mut vec_of_borrows: Vec<Ref<str>> = Vec::new();

    // Question: Can we get an iterator of Ref-wrapped items more easily?
    for i in 0..borrowed_vec.len() {
        let borrowed_item = Ref::map(Ref::clone(&borrowed_vec), |ref_vec| &ref_vec[i]);
        // Question: Is there an easier way to transpose Ref<'a, Option<T>> to Option<Ref<'a, T>>?
        // Question: What if I have a custom enum?
        match *borrowed_item {
            // Question: Can we not check that the borrowed item is of the Some variant again here?
            Some(_) => {
                vec_of_borrows.push(Ref::map(borrowed_item, |s| s.as_ref().unwrap().as_str()))
            }
            None => {}
        }
    }

    // This result is not important, just to check we did the right thing.
    let cat: String = vec_of_borrows.iter().map(|r| -> &str { &*r }).collect();
    assert_eq!("onetwo", &cat);
}

Basically I would like to do these operations on Ref-wrapped values:

pub use std::cell::{Ref, RefCell};

pub fn ref_filter_map<'b, T, U, F>(orig: Ref<'b, T>, f: F) -> Option<Ref<'b, U>>
where
    T: ?Sized,
    U: ?Sized,
    F: Fn(&T) -> Option<&U>,
{
    match f(&*orig) {
        Some(_) => Some(Ref::map(orig, |t| match f(t) {
            Some(u) => u,
            None => unreachable!(),
        })),
        None => None,
    }
}

#[test]
fn filter_map() {
    let data = RefCell::new("".to_string());

    let ref_val: Ref<String> = data.borrow();
    let opt_ref: Option<Ref<str>> = ref_filter_map(ref_val, |val: &String| match val.len() {
        0 => None,
        _ => Some(val.as_str()),
    });

    assert!(opt_ref.is_none());
}

pub fn ref_transpose_option<'b, T>(orig: Ref<'b, Option<T>>) -> Option<Ref<'b, T>> {
    match *orig {
        Some(_) => Some(Ref::map(orig, |t| match *t {
            Some(ref t) => t,
            None => unreachable!(),
        })),
        None => None,
    }
}

#[test]
fn transpose() {
    let data = RefCell::new(Some("I exist.".to_string()));

    let ref_opt: Ref<Option<String>> = data.borrow();
    let opt_ref: Option<Ref<String>> = ref_transpose_option(ref_opt);

    assert_eq!("I exist.", &*opt_ref.unwrap());
}

(Playground)

Without access to the internals of std::cell::Ref, implementing the operations is cumbersome. The closure by the ref_filter_map function must be evaluated twice which prevents marking it as FnOnce.

We can shorten your original code:

But I'm not too sure about the rest...

Well yes you are right. You can implement things on top of ref_filter_map and ref_transpose_option fairly easily. I for one would prefer the following.

use std::cell::Ref;

pub fn ref_transpose_option<'b, T>(orig: Ref<'b, Option<T>>) -> Option<Ref<'b, T>> {
    match *orig {
        Some(_) => Some(Ref::map(orig, |t| match *t {
            Some(ref t) => t,
            None => unreachable!(),
        })),
        None => None,
    }
}

pub fn ref_transpose_vec_iter<'b, T>(x: Ref<'b, Vec<T>>) -> impl Iterator<Item = Ref<'b, T>> {
    let count = x.len();
    std::iter::repeat_with(move || Ref::clone(&x))
        .enumerate()
        .take(count)
        .map(|(i, x)| Ref::map(x, |x| &x[i]))
}

pub fn extract<'b, T>(x: Ref<'b, Vec<Option<T>>>) -> Vec<Ref<'b, T>> {
    ref_transpose_vec_iter(x).filter_map(ref_transpose_option).collect()
}

(Playground)

The trick is implementing ref_filter_map and ref_transpose_option themselves. I don't see a way of doing it without the repeated checks.

Perhaps I shouldn't be doing this at all and I am abusing RefCell. Any input is welcome.

This looks like an abuse of RefCell. What problem are you trying to solve?


The reason I say that it looks like an abuse of RefCell is because RefCell is a lot like Mutex, it gives you smart pointers that track borrows. The smart pointers themselves are dereferenceable, and give access into the structure, but aren't meant to be stored or manipulated directly.

I am attempting to implement incremental computation of my glsl shaders. The shader code is generated from variables in my program and from files on disk. Whenever a file or variable changes I would like to re-compute the sources for my shaders and re-upload them to the GPU.

I got pretty far without RefCell but as soon as you have values in arrays I think you need it to provide more granular borrows. I was hoping to stay away from Rc until I discover why I need it.

I fully realize I'm reinventing the wheel and incremental computation has been researched for at least 40 years and I'm just trying to hack it together but I am doing it to deepens my understanding of Rust and appreciate libraries like salsa and adapton.

Unfortunately, I am now unable to decide how to handle #include ... directives in my shader. I keep all my sources in a Vec and collect references to elements in the vec while going through my dependencies. When I encounter an #include, I'd like to push onto the vec but I can't because I am holding on to these references. I thought adding a layer of indirection through Rc would help but then I could no longer return the collected references because they are tied to a cloned Rc and can't escape the function. I'll have another go tomorrow.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.