Lazy initialization vs. interior mutability

I have a type with a string value that is computed in lazy fashion; but once it is computed, it never changes. I've got an implementation, but I'm wondering whether there's a more idiomatic (or simply more clever) way of doing it.

I'm currently using an implementation like this:

pub struct Value {
    inner: Rc<RefCell<InnerValue>>,
}

struct InnerValue {
    string_rep: Option<Rc<String>>,
    ...
}

impl Value {
    pub fn as_string(&self) -> Rc<String> { ... }
}

Either the string exists when the Value is created, or it's computed and saved by a method the first time it's wanted. Clients need to be able to retrieve the string_rep on demand.

I'm not entirely happy with this implementation; it seems like I've got a whole lot of allocation going on, and it seems overly complicated. In particular, I've got that second Rc in Option<Rc<String>>. That's because I don't want the use of interior mutability to appear in the API (semantically, the Value is immutable). Rather than letting a stray Ref escape, the retrieval method calls RefCell::borrow, clones the Rc<String>, and returns it.

What I'm wanting is something where I can still compute and return the string_rep in lazy fashion; but I can provide a method like this:

impl Value {
    // I.e., the lifetime of the result is the same as the lifetime of the `Value`.
    pub fn as_string(&self) -> &str { ... }
}

Is there a canonical way to do this that doesn't involve RefCell? Is there a simple unsafe solution?

Indeed, once the value is known to have been initialized (e.g., no None in InnerValue), RefCell checks are superfluous.

For a manually checked version of RefCell, there is UnsafeCell (on top of which RefCell is built).

For this particular pattern, however, there is already a crate that does the unsafe for us: ::once_cell

6 Likes

I'll take a look at once_cell. Thanks!

This worked very well. I started using the ::once_cell crate, and then converted over to using UnsafeCell<Option<String>> directly. The UnsafeCell is created with either Some, or None; and it is queried in exactly one method which returns it if Some and sets and returns the string if None.

Yes, as long as you don't unsafe impl Sync or something like that it should be sound (hard to tell without seeing the code)

You can find it in here, if you're interested:

But I'm an old C programmer. I like not having to worry about safety, but I still know how if I need to. :slight_smile:

  • (Context: string_rep: UnsafeCell<Option<String>>)

  • this is indeed sound, good job; but it relies on Value not being Sync (else there could be a data race if two threads attempted to initialize it). I recommend you mention this fact somewhere in the NOTE:s, to "prevent" someone from adding an unsound unsafe impl Sync for Value {} later on.

    • Or even better, you could go and add an assert_not_impl!(Value, Sync) to explicitely make compilation fail if someone were to add such impl.
  • if slot.is_some() {
        return slot.as_ref().expect("string rep");
    }
    

    you can avoid this usage of .expect:

    if let &Some(ref inner) = slot {
        return &**inner;
    }
    

    or if you prefer to let Rust do stuff under the hood, you can just do:

    if let Some(inner) = slot {
        return inner;
    }
    
2 Likes

Thank you very much for taking the time to give me some style notes! Writing idiomatic Rust takes time to learn, and I'm still very much learning.

Regarding Sync, I'll see about adding the assert_not_impl. In fact, Value is part of a much larger system that isn't Sync either; the usual thing in multi-threaded TCL programming is to keep each TCL Interp in its own thread and send TCL commands back and forth. I suppose it would be possible to make the Interp Sync if I worked at it....