Getters on RefCell<Enum>, Option + Ref

pub enum Foo {
  I32(Vec<i32>),
  F32(Vec<f32>),
}

pub struct Bar {
  data: RefCell<Foo>
}

It's simple to write functions of type signature

impl Foo {
  get_i32(&self) -> Option<&Vec<i32>>;
  get_f32(&self) -> Option<&Vec<f32>>;
}

Now, if we try to do get_i32 and get_f32 on Bar, I run into the following problems:

impl Bar {
  fn get_i32(&self) -> Option<Ref<Vec<i32>>>;
  // this can't work, since to even know
  // if we are returning Some or None, we need
  // to examine the RefCell

  fn get_i32(&self) -> Ref<Option<Vec<i32>>>;
  // I can't get this to work with Ref::map since it expects
  // the function to return a &Option<Vec<i32>>
  // which ends up taking ref of a temporary
}

Question: is there a way to build get_i32 and get_f32 on Bar ?

I don't think there is any possibility of obtaining a Ref<T> given only a &T. You can only ever obtain Ref<T> by calling RefCell::<T>::borrow(), since the Ref type has no public constructor, and Ref::map requires the mapped function to return a reference, not a value possibly wrapping a reference.

I can see two(-ish) ways to rewrite this:

  1. "Shift the blame" (or burden) to the caller: just return a Ref into the inner Foo and let the caller use this ref for calling get_i32 on the inner Foo.
  2. Write your own map function that obtains the Ref internally and hides it, then calls the supplied closure only if the inner getter returned Some. A slight variant on this theme would be calling the closure all cases, but with an Option as its argument.

Here's a playground:

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

pub enum Foo {
  I32(Vec<i32>),
  F32(Vec<f32>),
}

impl Foo {
  fn get_i32(&self) -> Option<&[i32]> {
      match *self {
          Foo::I32(ref vec) => Some(vec),
          Foo::F32(_) => None,
      }
  }

  fn get_f32(&self) -> Option<&[f32]> {
      match *self {
          Foo::I32(_) => None,
          Foo::F32(ref vec) => Some(vec),
      }
  }
}

pub struct Bar {
  data: RefCell<Foo>
}

impl Bar {
  // First method: shift the burden to the caller
  fn as_ref(&self) -> Ref<Foo> {
    self.data.borrow()
  }

  // Second method: get rid of the ref altogether.
  // You wouldn't ref in public, would you?
  fn map<T, R, G, F>(&self, getter: G, func: F) -> Option<T> 
    where R: ?Sized,
          G: FnOnce(&Foo) -> Option<&R>,
          F: FnOnce(&R) -> T,
  {
    getter(&*self.data.borrow()).map(func)
  }
  
  // Or, if you always want the closure to be called:
  fn map_opt<R, G, F>(&self, getter: G, func: F)
    where R: ?Sized,
          G: FnOnce(&Foo) -> Option<&R>,
          F: FnOnce(Option<&R>),
  {
    func(getter(&*self.data.borrow()));
  }
}

fn main() {
    let bar = Bar {
        data: RefCell::new(Foo::I32(vec![1, 2, 3]))
    };
    
    // First method
    let inner = bar.as_ref();
    let maybe_i32 = inner.get_i32();
    let maybe_f32 = inner.get_f32();
    
    println!("shift the blame i32:                     {:?}", maybe_i32);
    println!("shift the blame f32:                     {:?}", maybe_f32);
    
    // Second method
    bar.map(Foo::get_i32, |x| println!("to call or not to call i32:              {:?}", x));
    bar.map(Foo::get_f32, |x| println!("to call or not to call f32:              {:?}", x)); // not printed at all

    // Or even
    let cloned_i32 = bar.map(Foo::get_i32, ToOwned::to_owned);
    let cloned_f32 = bar.map(Foo::get_f32, ToOwned::to_owned);
    println!("to owned and return i32:                 {:?}", cloned_i32);
    println!("to owned and return f32:                 {:?}", cloned_f32);
    
    // 2.5th method
    bar.map_opt(Foo::get_i32, |x| println!("always call but maybe with nothing i32:  {:?}", x));
    bar.map_opt(Foo::get_f32, |x| println!("always call but maybe with nothing f32:  {:?}", x));
}
1 Like

My intention is not to nitpick. I follow the rest of your post, but I don't understand the first paragraph, in particular:

I don't see why impossibility of &T -> Ref<T> (which I agree with) is relevant. I'm trying to understand if there is some argument / intuition that I am completely missing.

impl Bar {
  fn get_i32(&self) -> Option<Ref<Vec<i32>>>
  {
    let data = self.data.borrow();
    if let Foo::I32(_) = *data { // if data.get_i32().is_some()
      // Some(Ref::map(data, |foo| foo.get_i32().unwrap()))
      Some(Ref::map(data, |foo| match *foo {
        | Foo::I32(ref it) => it,
        | _ => unreachable!(),
      }))
    } else {
      None
    }
  }
}
4 Likes

This is the clever part that I did not think of.

The problem with that solution is that it requires repeating the logic of each Foo::get_*() function – and because you mentioned those getters, I was implying that the requirement was to use Foo's getters for implementing those of Bar.

Actually, this leads directly to why I made the comment about &T -> Ref<T> conversion:

Your getter functions on Foo return, for example, &Vec<i32>, and then you want to go to Ref<Vec<i32>>, so here T = Vec<i32>. That was the part I was talking about, again, because I mistakenly implied you wanted to go through Foo::get_i32().

2 Likes

Yes, having to repeat the match is not pretty, but is the simplest solution I'd say.

For the sake of completeness, I'll mention the CPS / callback pattern, since it allows you to do all kind of crazy things even when RefCells and Refs are involved:

impl Bar {
    fn with_i32<R> (
//     ^^^^ `with` instead of `get`
        self: &'_ Self,
        ret: impl FnOnce(Option<&'_ Vec<i32>>) -> R,
//                       ^^^^^^^^^^^^^^^^^^^^ "returned" value here
    ) -> R
    {
        let data = self.data.borrow();
        // we get to `ret`urn values that borrow locals!
        ret(if let Foo::I32(ref it) = *data {
            Some(it)
        } else {
            None
        })
    }
}

Obviously, it comes with an ergonomic hit for the caller, but when the rightward drift is acceptable, it becomes just a matter of getting used to it:

  • from:

    fn sum_i32s (bar: &'_ Bar) -> i32
    {
        let i32s = bar.get_i32();
        i32s.map_or(0, |it| it.iter().sum())
    }
    
  • to:

    fn sum_i32s (bar: &'_ Bar) -> i32
    {
        bar.with_i32(|i32s| {
            i32s.map_or(0, |it| it.iter().sum())
        })
    }
    
  • Playground

1 Like
  1. I understand the relevance of 'impossibility of &T -> Ref<T>' now.

  2. Sorry for the confusing original question. Using Foo's getters is not a hard requirement. I wrote getters for Foo solely to contract my inability to write getters for Bar, -- and Foo's getters returned a &Vec<T> because I didn't want to clone (and could not figure out anything else to return).

1 Like

Returning a borrow is indeed a good idea (if the caller wants to clone, let them do it), but in the case of Vec, you could return a shared borrow to the slice rather than to the intermediate Vec: &[T].

The ergonomic difference here would be most visible for someone wanting to treat None the same as an empty slice, since they would be able to chain .unwrap_or(&[]) to do that quite elegantly (compared to the currently required preliminary .map call, such as .map(|v| &v[..]))

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