Tips on how to read nested wrapper types in Rust

My sojourn with Rust has brought me to the point where I am now encountering code bases with values that have types like ``RefCell<Rc<RefCell>>` and I am at a loss at how to read such nested wrapped types.

I understand RefCell as a type that enforces the RWLock pattern at runtime. While Rc allows for multiple ownership using a reference counted pointer.

But when these types are combined like this. I am basically at a loss at how to interpret this.

To be more specific, case in point, the struct definition here which has two fields defined as this

pub struct Interpreter {
    pub globals: Rc<RefCell<Environment>>,
    environment: RefCell<Rc<RefCell<Environment>>>,

I mean how to read Rc<RefCell<Environment>> and RefCell<Rc<RefCell<Environment>>>? What functionality does these wrapping provides? Why is such wrapping even needed?

I mean how is RefCell<Rc<RefCell<Environment>>> different from Rc<RefCell<Environment>> in terms of what can be done or not done with their corresponding values?

The effect of any individual layer of generics is the same, no matter how many layers deep it is, or what's wrapping around it.

RefCell<T>: provide single-threaded, runtime-checked mutable access to a T via a shared reference. For cases where you cannot otherwise acquire a mutable reference by safe means.

Rc<T>: provide single-threaded, shared ownership of a T . Does not allow for mutable access to its interior.

Rc<RefCell<T>>: provide single-threaded, shared ownership of a RefCell<T>, which provides runtime-checked mutable access to a T. This lets you both share the T and mutate it.

RefCell<Rc<RefCell<T>>>: provide single-threaded, runtime-checked mutable access to an Rc<RefCell<T>>. The implication is that Interpreter is often/always only accessible via a shared reference, which would prevent mutation, but the author wants/needs mutable access.

In this case, I would assume that the author wants to be able to mutate Environments generally, so they consistently use Rc<RefCell<Environment>> wherever one is used. However, in the specific case of this field, they want to be able to swap which Environment is being used for the Interpreter, even when they only have shared access to it. The RefCell<Rc<RefCell<Environment>>> allows the Rc part itself to be swapped out without having to mutate the underlying Environment (which might be used somewhere else).

In contrast, globals doesn't allow you to swap out the entire Environment in one go with only shared access. Presumably because large modifications to globals is uncommon. Perhaps individual globals are changed one-at-a-time, but the whole execution environment is swapped out frequently?

Does that help?


Question to further help clarify things for me. Does the order matter? I mean Rc<RefCell<T>> vs RefCell<Rc<T>>

Yes, in exactly the same way that given fn double(x: i32) -> i32 { x*2 }, double(3) is not the same as 3(double). Generics are just type-level functions that take types as arguments and spit a new type out.

So in Rc<RefCell<T>>, the RefCell wraps the T, and Rc wraps the RefCell<T>. To get to the T, you have to go through first the Rc, and then the RefCell.


And semantically,

  • Rc<RefCell<T>> is a shared mutable thing. That is, thing which can be owned by multiple places at once and mutated by any of them with runtime checks.
  • RefCell<Rc<T>> is a mutable shared thing. That is, thing which is single-owned, but holds inside some shared state, which can't be mutated (since it's shared), but can be swapped.

The former is what you will usually see in single-threaded code with complex ownership. The latter is... niche, at best.


One way to think of this is that Rc<U> and Arc<U> are shared ownership data structures. There is only one U that is owned by all of the cloned Rc/Arcs. The one U isn't dropped until the strong count falls to zero. However, because of Rust's aliasing rules, these shared ownership containers can not hand out &mut U (exclusive borrows), they can only hand out &U (shared borrows).[1] All the owners can get &T at the same time.

Then you have shared mutation (aka interior mutability) data structures such as RefCell<T>, Mutex<T>, and RwLock<T> (and others we'll ignore here). They enforce Rust's exclusive aliasing rules at run time instead of compile time and thus enable mutating their contents even when you only have a shared reference to the container.[2] For example you can get a RefMut<'_, T> from a &RefCell<T> (and then a &mut T via the RefMut<'_, T>).

If your goal is to have multiple handles to a single item T (shared ownership), yet still be able to mutate that item, you need an Rc<RefCell<T>> (or Arc<Mutex<T>>, etc). Then you can go from Rc<RefCell<T>> to &RefCell<T> to RefMut<'_, T> to &mut T.

If instead you had a RefCell<Rc<T>> for example, you could get a &Rc<T> or &mut Rc<T>, but then you could only get a &T out of the Rc. So it cannot satisfy the goal.

Exclusive access to a shared owner of T still only allows shared access of T.

  • Owning RefCell<Rc<T> or having &mut RefCell<Rc<T>> won't get you a &mut T
  • So you'll generally never see this

Shared access to a shared mutability container of T can get you mutable access to T.

  • Having &Rc<RefCell<T>> can get you a &mut T

  1. Unless the strong count is exactly 1, which I will ignore in this post under the assumption that having many owners was the point of using these data structures. ↩︎

  2. Technically they are also relaxing Rust's shared aliasing rules via Rust's interior mutability primitive, an UnsafeCell. ↩︎


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.