Read-only field in RefCell

I am not good at English. Sorry if there are any funny expressions.


Problem

About RefCell<MyType> where MyType has read-only fields.
My goal is to get those fields with reference (&).
But from API, I can only get values wrapped with dynamic borrowing (Ref).
I think it's strange that read-only values are wrapped with Ref.

The way I don't like it

Of course, in the first place, I wouldn't have this trouble if I separate MyType into two types
(One with immutable fields, and the other with RefCell and mutable fields.)
But I think it is not cool to add type for this purpose.

My challenge with unsafe

I use unsafe to solve this problem.
But in my experience, when I use unsafe, I am usually confused.
I'm worried that I'm not making any sense this time, too.
So please check my code.

Here, fst_name field is read-only, and snd_name field is write-enabled.
Key point: fst_name field is wrapped with my original type Rom.

use rom::Rom;
use std::{cell::{Ref, RefCell}, ops::Deref, rc::Rc};

fn main() {
    let x = Person::new("John", "Smith");
    let y = Person::new("James", "Brown");
    x.borrow_mut().best_friend = Some(y);
    assert_eq!(x.borrow().best_friend_fst_name().unwrap(), "James");
    assert_eq!(x.borrow().best_friend_snd_name().unwrap().as_str(), "Brown");
}

pub struct Person {
    fst_name: Rom<String>,
    snd_name: String,
    best_friend: Option<Rc<RefCell<Person>>>,
}

impl Person {
    pub fn new(fst_name: &str, snd_name: &str) -> Rc<RefCell<Self>> {
        let fst_name = Rom::new(fst_name.to_string());
        let snd_name = snd_name.to_string();
        let ret = Self { fst_name, snd_name, best_friend: None };
        Rc::new(RefCell::new(ret))
    }

    pub fn best_friend_fst_name(&self) -> Option<&str> {
        let bf = self.best_friend.as_ref().map(|x| x.deref())?;
        Some(Rom::map_ref_cell(bf, |x| &x.fst_name))
    }

    pub fn best_friend_snd_name(&self) -> Option<Ref<String>> {
        let bf = self.best_friend.as_ref().map(|x| x.deref())?;
        Some(Ref::map(bf.borrow(), |x| &x.snd_name))
    }
}

mod rom {
    use std::{cell::RefCell, ops::Deref};

    pub struct Rom<T: ?Sized>(T);

    impl<T> Rom<T> {
        pub fn new(value: T) -> Self {
            Self(value)
        }
    }
    
    impl<T: ?Sized> Rom<T> {
        pub fn map_ref_cell<C>(cell: &RefCell<C>, f: impl Fn(&C) -> &Self) -> &T {
            &f(unsafe { &*(cell.as_ptr() as *const _) }).0
        }
    }
    
    impl<T: ?Sized> Deref for Rom<T> {
        type Target = T;
        fn deref(&self) -> &Self::Target {
            &self.0
        }
    }    
}

No, this is a good way to do it. There are many cases in Rust where adding a type is the right solution. RefCell itself is an example of this: Ref and RefMut are separate types that have no purpose other than being pieces of using a RefCell. And non-public inner structs are even more common.

Another good option is to put the individual fields in RefCells or not:

pub struct Person {
    fst_name: String,
    snd_name: RefCell<String>,
    best_friend: RefCell<Option<Rc<Person>>>,
}

You would do this instead of having a RefCell around the whole Person.

Person as it's written is sound, but it doesn't have any actual mutation methods. Rom is unsound, because there is nothing that guarantees the field that it provides access to is one that won't be mutated by other means.

3 Likes

Thanks for your reply.

I was surprised that type separating is popular approach.
Until now I have been thinking that if the data flow is the same,
I should keep it in one place as much as possible.
However, immutable and mutable are clearly a big difference,
so separating them might be a natural thing.

Also, I feel it fresh to wrap each field individually with RefCell.
It may be a little bothersome, but except that,
it seems to be a good idea to be able to control the fields in detail.

Finally, about Rom becoming a pattern unsound.
Oh... After all, unsafe is difficult (I was concerned about rewriting
the contents, but it is meaningless if entire Rom is replaced...).

1 Like

Exactly.

Yes. In general when designing Rust APIs, it’s important to keep in mind that wherever some &mut T is available, it's an opportunity to swap the entire T for another unrelated or new T. You can only enforce some relationship between a struct and a field of the struct by not ever handing out a &mut reference to the field.

1 Like

I see, thanks for the very clear guideline!!! :star:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.