Hi Rust experts. I am writing some code that is fairly heavily multi-threaded, and as a result have many Sync/Send structs with methods with a (&self, ...) signature (rather than (&mut self, ...)) that mutate their state internally using RwLocks, Atomics, Mutexes, etc.
One thing that has been difficult for me to reason about is how to avoid logical "surprises" wrt interior mutability. For example, it is very easy for me to reason about a method like fn increment(&mut self) and understand that it mutates state and has side-effects, but less so with a method like fn increment(&self) that does so with (necessary) interior mutability and thus has side-effects when one might infer that it is pure, aside from the naming obviously.
I know that good naming and docs can help mitigate this surprise factor, but are there other ways? Are there good guidelines in some well established crates?
If anyone could point me to some "best practices" in this area or even share some ideas on how to manage it, I would appreciate it.
Unfortunately, there isn't an established way of separating functions with shared mutability.
Implementations have to explicitly support interior mutability, so this is usually known from the context.
BTW, &self shouldn't be understood as immutable/pure. I think Rust's naming of &mut creates wrong expectation along the lines of (im)mutability. &mut is exclusive, and & is shared (although no matter how you cut it, there are some exceptions).
I fall under that unfortunately. I’m looking for “bastion’s” of pure functions that I can quickly reason about… get frustrated to discover a side effect (and call it out in a code review)!!
I agree with your point re shared and exclusive borrows instead of mutable vs immutable. That’s how I model it. The model extends to ownership, where mutable/immutable gets clumsy.
To the main topic: Is there not a way to use something like a Mutex to avoid interior mutability? I seem to recall a talk by a rust super star that suggested avoiding interior mutability because it circumvents the intent - you can’t reason about it from the type signature. It’s also like using Any in TS or Python - it defeats the purpose.
Mutex works through interior mutability. It contains an UnsafeCell, which is the root of all interior mutability that doesn't involve raw pointers. RwLock (of which Mutex is effectively a restricted version) is the multi threaded counterpart to RefCell.
Thank you. Yes. Does doing so (using Mutex) make your intent more transparent?
I should reword a few things. Sometimes we can’t avoid the benefits of interior mutability. If you’re going to use it, be explicit and use the rust machinery to do so?
I suppose it could, but uncareful use of mutexes can cause problems. Specifically,
If the mutex is actively used by multiple threads and one of them keeps it locked for longer than necessary, it can slow down the application. Thus, one should be somewhat careful about when the MutexGuard gets dropped.
If multiple mutexes are in use by the same threads simultaneously, then it's possible to get a deadlock (thread 1 has lock A and wants lock B; thread 2 has lock B and wants A).
Both of these factors mean that if a problem needs a mutex, then it is good practice (in my opinion) in many cases to encapsulate the Mutex in some new type that provides only the intended usage pattern (and never returns a MutexGuard) through carefully designed methods. This consideration outweighs the consideration of making the use of interior mutability more visible, because it protects against nondeterministic bugs and performance bugs.
And you certainly shouldn’t introduce a Mutex around a type that is already interior mutable, because then you’re adding mutex problems you don’t need at all.
Virtually, you can implement and enforce any semantic you would like using this. And thanks to Rust optimizer, maybe with some help from #[inline(always)] and/or #[repr(transparent)], this all makes no performance differences.
Thanks everyone, this is a good discussion. Obviously the distinction between mutable/immutable and exclusive/shared is an important, and the naming of mut isn't the greatest given it has somewhat overloaded meaning (given it means exclusive rather than mutable), so I'm with you all there, but I think we can all agree that ship has well and truly sailed.
If I were to rephrase the question, I might re-ask it as:
"Are there any conventions/tools/markup/whatever to indicate that a fn(&self, ...) method has side-effects because of interior mutability?"
or
"How do I ensure that someone seeing a fn(&self, ...) method knows that it is impure?"
For me, it's really about ensuring that my teammates and future self understand that a method might have side-effects even if it is not a fn(&mut self, ...), which is the obvious tell.