Is the use of interior mutability part of an interface?

Is it correct to say that when defining a trait/interface, you must decide whether implementers will use interior mutability? In other words, its use is not merely an implementation detail?

When I first learned about interior mutability, I thought it was an implementation detail. However, as I experiment with it, I see that you need to decide whether the trait methods take &self or &mut self, and whether you pass around dynamic objects as Rc<dyn T> or Rc<RefCell<dyn T>> (or their multithread equivalents).

Whether the interface requires &mut or & is, as you have observed, public and quite an important design decision. Whether a type actually uses interior mutability is (mostly) an implementation detail. It can be different in either direction:

  • A trait with &mut self can still be implemented on an interior mutable type.
  • A trait with &self can be implemented without using interior mutable state, e.g. by doing nothing (perhaps it is the implementation for the () type).

These do not always make sense in all situations, of course. But all that is guaranteed about the relationship is that if the interface requires &self, then it is certain that the implementation does not require exclusive access.[1]


  1. But it still might be exclusive in practice by using a mutex! â†Šī¸Ž

3 Likes

There are considerations beyond shared (interior) mutability. You can't call a &mut self method while a reference to a field is active, for example.

There's always a tradeoff that will be nice (or unnoticed) for implementers for which the receiver is a natural fit, and potentially annoying for those it does not.

2 Likes

Yeah, I guess I just ran into one of those annoying situations, which prompted me to think about this and ask the question.

I had an trait ListModel that was taking &self, even for a couple optional methods like delete(&self, i: usize). The impls were all using interior mutability and passed around in Rc<Thing>. And code using the ListModelrefers to it through a Rc<dyn ListModel>. But then I hit a situation where I have a model in Arc<Mutex<Model>>, which I wanted to adapt to a ListModel, but it was not using interior mutability, instead relying on users to go through the Arc<Mutex<_>> lock.

One trick to keep in mind is that you can define a trait using &mut self, then implement it for &MyInteriorMutableStruct. For example, the standard library has impl io::Write for &fs::File. This requires the caller to add the & explicitly to use that implementation, but allows you to use the same trait and generic code with both implementors requiring &mut self, and with implementors that do not in situations where the &mut is impossible to get.

However, this won't help if you need a long-lived dyn. For that, you might define two traits, a ListModelRef and ListModelMut (and you can impl<T: ListModelMut> ListModelRef for Mutex<T>). Of course, that is added maintenance cost of keeping those two in sync and converting, so only do that if you need it.

3 Likes

Yes, use &self for mutation in objects that are shared (often used in caches, since a cache that can be used from one place only for one thing at a time is very limited).

Use for lazy initialization, and situations where the interface isn't for writing/mutating, but needs to do some mutation for housekeeping.

Conversely, you can artificially require &mut for interfaces where you want to enforce one-at-time semantics without locks (or making locks the caller's problem), even if you don't need the &mut. For example, when writing to a file or printing output you don't want multiple callers invoke writing to the same file at the same time and randomly interleave the data.

Arc sometimes appears in APIs where you have shared objects and need to spawn threads. A server could have pub async fn listen(self: Arc<Self>).