Why the `mut` keyword is not required when declaring a variable of type `std::sync::Mutex` when you know the value of that variable will get mutated

While studying The Book today, I got confused about why Listing 16-12 is allowed to mutate the value of m despite m being declared without the mut keyword:

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5); // Lacks the `mut` keyword here

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {m:?}");
}

The Book: Listing 16-12: Exploring the API of Mutex<T> in a single-threaded context for simplicity (Comment added for emphasis)

That code prints the following to Standard Output:

m = Mutex { data: 6, poisoned: false, .. }

This shows that the value of m's data field got mutated from 5 to 6, despite m being declared without the mut keyword.

This is in contrast to the concept expressed in Section 3.1 of The Book, which says that variables are immutable by default, and that it is important for mutable variables to have the mut keyword in front of the variable name if the value is subject to change.

For example, if I try running the following code, the compiler will reject it, as I am trying to mutate a non-mut variable's value:

fn main() {
    let a = 5; // Again, no `mut` keyword, but the compiler won't be happy this time
    let b = &mut a;
    
    *b = 10;
    
    println!("{}", b);
}

I rightfully get the following error in this second scenario:

   Compiling playground v0.0.1 (/playground)
error[E0596]: cannot borrow `a` as mutable, as it is not declared as mutable
 --> src/main.rs:3:13
  |
3 |     let b = &mut a;
  |             ^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
2 |     let mut a = 5; // Again, no `mut` keyword, but the compiler won't be happy this time
  |         +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `playground` (bin "playground") due to 1 previous error

I was puzzled over this. How could we let Mutex get away with not having to follow the same rules as the other variable types?

As I kept reading, however, I eventually found that the answer has to do with interior mutability. This is because Mutex<T> provides interior mutability, as the Cell family does. I'm still in the process of trying to figure out more about the motivation/reasoning behind interior mutability, but I at least have a starting point now.

I would love to hear your thoughts on the advantages of interior mutability (as opposed to regular mutability), and why the normal requirement for an explicit mut declaration is deliberately waived for cases of interior mutability.

Hopefully this post helps someone who is scratching their head (like I was) to be able to have their "Oh, that's why" moment a little sooner.

When something is confusing, keep reading! :crab::heart:

1 Like

One way to look at interior mutability is that is allows "shared mutation". How is it shared? By allowing you to mutate without being the owner or having a &mut (exclusive) reference. How is it mutable? With UnsafeCell and various techniques that use it.

3 Likes
  • The borrow checker rules work on "regular" references (& and &mut), but not raw pointers.
  • Working with raw pointers is inherently more dangerous (e.g. you can dereference them while they point to uninitialized memory or mutate said memory in parallel without the necessary thread synchronization primitives) - this is why "regular" references are preferred.

So those are two different instruments - one is less powerful, but safer - the other one lets you do anything, but it's error prone. However to dereference a raw pointer you need the unsafe keyword to signify that the developer promised to uphold the invariants instead of the compiler (like not dereferencing uninitialized memory).

So there's a middle ground (and the right way to work in Rust in my opinion) - borrow checker limitations (where correct code is rejected) can be solved by using the power of unsafe to create new primitives, but by providing a safe API on top of it - even if the primitive is using unsafe internally it should still be impossible for safe Rust code to invoke undefined behavior due to the safe API limitations.

Notice how Mutex allows mutation - the lock() method gives you a MutexGuard and self.lock.data (the data behind the mutex) is UncafeCell:

impl<T: ?Sized> Deref for MutexGuard<'_, T> {
    type Target = T;

    fn deref(&self) -> &T {
        unsafe { &*self.lock.data.get() }
    }
}

Basically the safe Mutex API won't allow you to get two MutexGuard instances, so it's not possible to mutate in parallel. If you try - the second lock will wait until the MutexGuard from the first lock is dropped (look at the general RAII concept for intuition on this resource management technique).

Another popular interior mutability primitive is RefCell which relaxes borrow checker limitations by making them happen in run time instead of compile time - however in this case the safety is enforced by panicking if you try to get a mutable reference and another reference in parallel (as opposed to lock which will deadlock before letting you get that far).

  • If you wonder how a panic is safe - it's much safer than undefined behavior which means "the compiler will not only not guarantee a predictable outcome, but it will proactively make use of such lack of guarantees for optimizations, so the developer should expect anything" - look around for articles like "undefined behavior can erase your disk" :smiley:

So the semantics of the various primitives will vary as well as their approaches to enforcing safety, but the summary is that the borrow checker will inherently be more limited than working with the full power of raw pointers and we need a way for safe Rust to make use of safe primitives without dropping into unsafe - this is interior mutability.

3 Likes

It's true, most learning material (including the Book) start with a "mutable versus immutable" presentation of the language, and then later say "oh except this Moon sized hole called interior mutability!".[1] (Then, if you learn like I do, you have to go re-learn 2/3rds of the material while keeping in mind this newly revealed truth.)

But the reality of the language is much closer to "exclusive versus shared". For example, &mut _ are exclusive references, and it's UB to alias an active &mut, even if no mutation occurs. Think of it as "mutually exclusive". &_ are shared references. If there's an UnsafeCell behind it, mutation may still be possible. This is documented as "interior mutability", but you can also consider it to be "shared mutability". And as covered in the Brubeck article, shared mutability also enables the implementation of shared ownership types, like Arc<_> and Rc<_>.

It wouldn't surprise me if it is true that newcomers overuse interior mutability before learning more Rustic design patterns. However, I think it gets a bad rap overall.[2]

There are even official docs that poo-poo the use of interior mutability generally, but the reality is that it is all over the place (though often wrapped up in such a way you may not notice). That is, even if you're not using it "directly" in your own types,[3] there's a good chance you'll still end up using it via:

  • shared ownership types (Arc<_>, Rc<_>)
  • thread handles
  • OnceLock<_> and friends
  • stdin and stdout accessors
  • notionally, many other OS primitives[4]

And it can even be useful in ad-hoc situations.

These are reasons why, while I'm on board with encouraging designs that avoid unnecessary shared mutability, I find calling it a "last resort" to be hyperbolic.

Shared mutation primitives provide a safe abstraction for what would otherwise be a lot of unsafe and raw pointers, most likely. It also maps well to how many system interactions (such as file handles) work.

In my experience, it's also not uncommon for people with an initial reaction against "hidden" shared mutability in Rust -- like in a generic -- to really be okay with some types of shared mutability, but not others, when pressed. Like Rc<_> without RefCell<_> is fine for some reason. But there's no formal difference between the "okay" types and the "bad" types. Rc<_> needs shared mutability for its reference counting to work.

First some words on what the mut declaration is and isn't about. Note that while &_ and &mut _ are two different type with important differences, let x: T and let mut y: T both declare variables of the same type T. In this case, the mut or lack of mut is a property of the binding -- the variable -- and not a type-level difference. Without a mut binding, you cannot

  • overwrite the variable once initialized
  • take a &mut to the variable

But those are about the only limitations. For example, you can move the variable to a new binding -- including a new mut binding. So if you have a let s = "hello".to_owned(), for example, the data owned by the s: String is not intrinsically immutable, even though there was no let mut and no shared mutability.

It's just that binding which has the restrictions. Which is still useful, but perhaps not as powerful as some newcomers believe.

If you have a compiling program, you can change every non-mut binding to a mut binding, and it won't change the semantics of the program (but will generate many warnings).


Next note also that the mut binding requirement is also waived for &mut _ in a sense. You can have an let x: &mut T = ... and mutate through the x. You don't need let mut x: &mut T = ... You just can't mutate (or overwrite) the x itself.

Having pointed that out, let's try a warm up question. Should mut be required in the arguments here?[5]

fn example(rc: &Rc<str>) {
    let _my_rc = rc.clone();
}

And if so... how would that be annotated? The only binding is rc, which is a shared reference that does not need to be mut. *rc is what contains the shared mutability... in some private field we can't name. rc: &mut Rc<str> changes the type of the argument and which programs can compile.

Warm up question 2: should this required mut t?

fn example<T: Clone>(t: T) {
    let _ = t.clone();
}

There might be shared mutability involved.

And another warm-up question: Should it be required here?

pub struct OffAndOn(pub bool);
impl fmt::Debug for OffAndOn { ... }

fn example(oao: OffAndOn) {
    println!("{oao:?}");
    println!("{oao:?}");
    println!("{oao:?}");
}

This may also disguise a form of interior mutability, in a sense.


So: why isn't let mut required when there's shared mutability "present"?

First we'd have to nail down what being "present" means. If we want to include things like managing state via globals, leaked pointers, transmuted types, system resources like file handles, or foreign data over FFI... there's no way to track shared mutability being present without a full blown effect system that conservatively considers all of those things to be a form of shared mutability. In order to be accurately require mut, that effect system would have to permeate everything. Otherwise it wouldn't be able to apply in the face of things like generics. It would require some global analysis, and just isn't practical.

So that's one reason.

Let's scale back our ambitions. How about, we only track whether or not there is an UnsafeCell<_> in some field. As it turns out, there's a trait for that. Hmm, it has this note though...

whether a type contains any UnsafeCell internally, but not through an indirection

Through an indirection, what's that mean? It means that, for example, Box<Cell<u32>> is Freeze. That doesn't seem to really be what we want...

fn main() {
    // This one requires `mut`...
    let mut a: Cell<u32> = Cell::new(0);
    // ...but this one doesn't?
    let b: Box<Cell<u32>> = Box::new(Cell::new(0));

    a.set(1);
    b.set(1);
}

Freeze's job is not to forbid reachable shared mutability, its job is to check if types can be put in read-only memory -- to check if they have immediate shared mutability.

If you wanted to tackle reachable shared mutability, you're heading back onto the messy situation discussed above. It's possible to store a *const Cell<_> as a *const () for example. If you had a custom Box<_>-like type, the compiler would have to again do some sort of deep analysis of all related code to (try to) detect shared mutability. It can't just check the field types.

...and even if there was a trait for reachable shared mutability, it still wouldn't really be what you want in the generic use case; if you didn't have a DeepFreeze bound, the generic could still contain or not contain shared mutability.


Ultimately I feel the reason that there's no simple way to detect (or forbid) shared mutability is that forbidding mutation isn't the goal. Quoting Niko,

it’s become clear to me over time that the problems with data races and memory safety arise when you have both aliasing and mutability. The functional approach to solving this problem is to remove mutability. Rust’s approach would be to remove aliasing.


  1. With fun contradictory statements like "Interior mutability is a design pattern in Rust that allows you to mutate data even when there are immutable references to that data" ↩︎

  2. I feel this at least partially comes from having a "mutable vs immutable" mindset or presentation, and then having to admit and explain the significant exceptions. ↩︎

  3. Mutex<_>, RefCell<_>, atomics... ↩︎

  4. which is how this implementation can exist ↩︎

  5. Function arguments are patterns that can bind variables, the same as let .... ↩︎

5 Likes

I would give you an answer that goes beyond Rust and covers also struggles that functional programming have and that plagues all other languages, too.

  1. Shared mutability is “root of all evil”. Almost all the bugs (90% if not 99%), almost everything “bad” that happens to your programs, almost all problems that one may imagine… may be traced to [ab]use of shared mutability.
  2. Unfortunately some amount of shared mutability is actually needed to solve most practical tasks. Think about this very forum: how useful would it be if we wouldn't have shared mutability here and everyone would have only been able to see their own posts?

This means that all languages have shared mutability, somewhere, but “good” languages mark and control it tightly.

And [this time specifically in Rust] terminology is inconsistent: instead of splitting ht world into “unique” and “shared” parts and then splitting them into “mutable” and “immutable” parts the two most common “safe” parts (these being “unique and thus mutable” and “shared and thus immutable”) got names “mutable” and “immutable”.

And then you get “shared mutability” (rare, crazy dangerous, yet oh-so-needed) thing… that's falls into “immutable” space.

This is just an unfortunate terminology, though. What Rust really does have are shared and unique modes and shared is, normally, immutable and that's why unique mode got mut keyword.

There was an attempt to fix it… but it went nowhere, thus we are stuck with this unfortunate terminology.

I just wish tutorials would stop using it: these are “shared” and “unique” things, not “mutable” and “immutable” ones… but maybe saying all that would just be too confused with mut as keyword. IDK.

But the core thing that you have to remember is that duality: shared mutability is both the most dangerous thing in a programming… and yet it's also something that we need to make our programs useful.

1 Like