I take it you mean mut
bindings like let mut
. In which case, this is only sort-of true even ignoring shared (interior) mutability. You can mutate through a let non_mut_binding = &mut foo
, you can move non-mut
bound variables to mut
bound variables, and you can end up calling the non-trivial destructor of non-mut
bound variables too (Drop::drop
takes &mut self
).
Some people feel very strongly about the importance of mut
or non-mut
bindings, but I consider them to basically be a hard-required lint. You can't rely on it without examining the rest of the code (even ignoring shared mutability), and mut
bindings doesn't show up in rustdoc
output, because ownership and the ability to move is a stronger capability than a single mut
binding.
(The lint boils down to: you can't overwrite or take a &mut _
to a non-mut
bound variable. The rest is fair game.)
Unfortunately, most learning material -- including The Book -- presents a "mutable or immutable" perspective. But "exclusive or shared" is much closer to the reality when it comes to Rust. For example, you've discovered that &_
references are not immutable (but they are shared). Similarly, &mut _
references demand exclusivity (even if you don't mutate through them). Here's an article on the topic. The article also links to this pre-1.0 post by Niko, which has one of the best summations of this topic IMO:
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. This gives us a story to tell and helps to set us apart.
I.e. it's never really been about immutability.
That's impossible without some global, deep effect system. You can be a structure that just holds an integer, but that integer could represent a pointer or a file handle, etc, that can effectively demonstrate shared mutability.
Moreover, it's probably not something you actually want for multiple reasons. Shared mutability lies behind every synchronization primitive, and (though outside the language) behind many OS primitives as well. You're making use of it with every println!
, Rc
, Mutex
, File
, atomic, etc.
As a more concrete example, I had a conversation with someone who was fine with the shared ownership of Rc<T>
on some level since it only yields &T
and not &mut T
. But how is the reference counting implemented? It's implemented with shared mutability of the counter. They were of the opinion that the shared mutability of the counter should somehow be magically permitted because they only really "cared" about the T
. But there's nothing at the language level to make the shared mutability of the counter special. They had some human-reasoning based desire for immutability that didn't translate to the technical reality.
I do feel there are solid improvements to be made for newcomers by changing learning material such as The Book to present the shared-vs-exclusive perspective and to stop presenting a misleading view of immutable-vs-mutable.
Indeed, if you wanted to be sure there was no shared mutability anywhere, you would have to know not only the fields, but every implementation, arguably including foreign implementations, as one can always stash some global token based on a type somewhere and create some sort of impure behavior off of that.
That being said, it's reasonable to say things like "&str
is not just a shared reference, but a truly immutable reference, because str
contains no shared mutability".
However, in a generic setting, there is no way to know if the generic has shared mutability. And this is fine. Your generic consumers want to be able to utilize shared mutability. They don't want to see some error about using Arc<str>
instead of &str
, say. Or some type of their own that features shared mutability more directly.
One has to undergo an annoying mental shift and relearning phase if they were taught the immutable-vs-mutable perspective. Certainly I did. But at least personally, things were much improved after I made the shift, and realized that shared mutability was not the devil, and was present in wide swaths of the standard library and ecosystem.