Confusion about mutability in bindings and references

Edit: TL;DR: Why is it OK for atomic variables that have not be declared with let mut to still be mutable?

The Question

After following some discussion on Internals, I came to the conclusion that either I'm misunderstanding something with regards to mutability in bindings and references, or there is some arguable (slight) inconsistency in the language.

The Rust book says:

In Rust, the compiler guarantees that when you state that a value won’t change, it really won’t change.

Hence one needs to declare a variable with let mut in order to modify it, or pass it as a mut reference. But atomic types (seem to) break this promise:

let v = 8u8;                     // This is immutable
let a = AtomicU8::new(8u8);      // This isn't

foo(&mut v);                     // error, because immutable
a.store(9u8, Ordering::Relaxed); // but this is fine and dandy?!?

In fact, if you declare the atomic variable with let mut you even get a warning: variable does not need to be mutable , which furthers the confusion (at least mine).

(Disclaimer: I do know that AtomicU32::store takes &self, which is why it compiles.)

The Advanced View of Things

The explanation for this is that it's not really about mutability, but about shared vs. unique (quote below by @HadrienG, https://docs.rs/dtolnay/0.0.8/dtolnay/macro._02__reference_types.html):

However, that diction then doesn't explain why one needs let mut to assign or modify variable values, even if there is no sharing involved. IOW, if we rewrite it accordingly:

let v = 8u8;           // no `let mut`
v = 9u8;               // error, even though no sharing (but still understandable)
foo(&unique v);        // error, even though no sharing (why?)

which moves the confusion to the previous "simple mutability explanation".

Suggestion

IMHO, to be really consistent, we'd need 3 reference kinds:

  • immutable (current &)
  • mutable unique (current &mut)
  • mutable shared (what atomics do)

To illustrate, using &mut_shr for the new third kind of reference.

let iv = 8u32
let ia = AtomicU32::new(8u32);

read(&iv);                          // fine
modify(&mut iv);                    // ERROR: not mutable
// AtomicU32::store now takes `&mut_shr self`
ia.store(3u32, Ordering::Relaxed);  // ERROR: not mutable (NEW!)

let mut mv = 8u32;
let mut ma = Atomic::new(8u32);     // `let mut` now required for modifications

read(&mv);                          // still fine
modify(&mut mv);                    // fine
ma.store(3u32, Ordering::Relaxed);  // now fine as well

Of course, the benefits of adding such a third reference type likely don't come close to the complications it'd created. OTOH, this should really only be required for very few library types, and can likely be restricted to the self parameter.

Question

I'm not really advocating we should make this change - I just would like to know whether this all makes sense, or am I really really misunderstanding something?

What it comes down to is that the line in the book

In Rust, the compiler guarantees that when you state that a value won’t change, it really won’t change.

is a simplification.

Things like Atomic*, Mutex, Cell, and other types all boil down to UnsafeCell under the covers, and expose "internal mutability" or "shared mutability".

At the low, memory-model level, Rust does actually have ponintee-mutable shared references. Those are &UnsafeCell<_>. At the high level, it's typical to encapsulate shared mutability and present a consistent identity through a shared reference, unless the point of the type is to expose shared mutability.

4 Likes

Don't think of it as mutable vs immutable. It's exclusive vs shared. You can change a shared atomic.

let mut is just about the variable name, not the content. It's a very light lint that has nothing to do with typesystem or data mutability.

let x = vec![];
{x}.push(1); // valid, because it doesn't use `x`, but a result of `{}` block
5 Likes

Heavily Edited:

I do think I got the gist at least, but my issue is not with exclusive or shared references per se: The reasoning why Atomic* use &self is perfectly cromulent, IMHO. My issue then, I guess, is what let and let mut are really supposed to mean.

We don't, that's the whole point of these types. Atomics, and other concurrency primitives such as Mutex, can guarantee safety because of how they are implemented internally. And, as I usually say, if it can be implemented as a library type, it shouldn't be a core language feature.

https://danielhenrymantilla.github.io/posts/2019-02-24-mutation-part-2-to-mut-or-not-to-mut/

1 Like

Well, UnsafeCell is hardly a library type, being a lang item and all. I guess what @rolandsteiner is trying to say is, it would be more consistent if &_ always meant shared immutable, in the same way that &mut _ always means exclusive mutable. The way to achieve this is with a new reference type, let's call it &mut_shr _ that replaces all usages of UnsafeCell.

1 Like

(Sorry for the heavy back-and-forth editing in my previous reply)

It seems my original post was a classic XY-problem statement. :pensive:

First, I do understand the internals about why Atomic* et al work with let and &self, and about UnsafeCell and all that (at least I think I do). And after these replies, I now have a better understanding that let really is more of an immutability lint than a guarantee.

So I guess what it really boils down to is about what guarantees does let really give vs. let mut, and which restrictions do both entail, and is the current state ideal?

To repeat the TL;DR I added to my original post: Why is it OK for atomic variables that have not be declared with let mut (but just with let) to still be mutable?

No guarantees, it's just to allow the compiler to generate lints that work 90% of the time. In the absence of shared mutability.

1 Like

Soooo, is there a way for me to say: "I have this here bit of AtomicU32 data. You can look at it, but you absolutely, positively mustn't modify it!"?

And how/why would it be better to retrospectively introduce a language element that is identical to an already-existing one? Why change things like this? Just for the sake of syntax? I'd disagree with that on several grounds.

You can make a wrapper ("newtype") around it which only exposes an immutable interface.

I never suggested adding it along side UnsafeCell, if we could restart and use &mut_shr from the start... Not that this is should/will ever be done. Maybe I could have been more clear about this.

By now I may be missing the forest for the trees, but the following still works.

use std::sync::atomic::{AtomicU32, Ordering};

struct S(AtomicU32);

impl S {
    fn get(&self) -> &AtomicU32 { &self.0 }
}

fn main() {
    let a = S(AtomicU32::new(5));
    a.0.store(17u32, Ordering::Relaxed);
    a.get().store(19u32, Ordering::Relaxed);
}

It seems one would need to write a full encapsulation struct, that completely hides away the atomic value (?). But that defies the point of my question: is there really no way to make an Atomic* immutable?

Yes, what I was suggesting is exactly to make a newtype that exposes only get() (etc.) methods while completely hiding the inner Atomic<…> type. In fact, I'd argue that a newtype that is just exposing the whole underlying representation is a leaky abstraction, an anti-pattern, and quite pointless. There might be a better/easier way, but if so, I don't know about it.

2 Likes

The question for me is then: What is the use of controlling this property?

  • If the importance is to uphold a user-defined invariant then you can write a newtype wrapper such as the one which you have already discovered.
  • For the correctness of memory mapping of statics, there is a compiler internal auto-trait called Freeze which captures a very similar property. Note that this is a trait implemented for types though and not a special reference type. The issue requesting making it public is here but has potentially large consequences on the core of the language and so requires good motivation and specification.
  • If the concern is suboptimal optimization then remember that the only hinderance comes when crossing from the outside of a &_ to the inside of an UnsafeCell, such as happens in Cell, RefCell, Atomic. Where it really matters, you'll want to take care not to make heavy use of these types. This is also the underlying reason for considering usage of RefCell unclean.
5 Likes

Thanks, all! I think I got the gist now.

I do realize this is more of a theoretical concern, open to "when do you really need it?" questions, and from a practical point of view I even agree. Still, I guess I was hoping that Rust's immutability story from a language design point of view was a bit more consistent.

As it is, the line

In Rust, the compiler guarantees that when you state that a value won’t change, it really won’t change.

perhaps should come with a disclaimer about Atomic* et al, referring to an advanced usage section for those.

1 Like