Well, such a lint would be fairly useless in those (common) situations when the expression itself already clearly indicates that it’s a mutable reference. E.g. things like
I suppose that a lint could assume that the _mut naming convention can always be followed and check that there is a mut somewhere in the declaration, including the initial assignment, either the keyword or part of an identifier.
Or maybe a /*mut*/ comment could disable the lint too for cases where changing the identifiers is too much of a pain.
Given a loose heuristic like that, it might be best as an opt-in lint in clippy.
While I can say that it is sometimes annoying to jump between let mut and let while testing things out, I think the readers' perspective is somewhat understated.
let x = &mut T::default();
x.foo();
When I read this what I see is "whatever x is it will name the same memory" and not whether it can be mutated or not. It doesn't matter if the type behind it is &mut, x will always name to the same piece of memory. In contrast to:
let mut x = &mut T::default();
x.foo(); // could mutate value
x = &mut T::default(); // "mutates" the label
When there's let mut I know I should look for a line where the object could change (assuming unused_mut is on). Though, one could argue that there should be a difference between a mutable binding and mutable value so syntax and semantics would be a bit more consistent especially around &mut values (which would probably mean even more mut to annotate ).
no rebinding -- no mutation = let x = &T; let y = T;
no rebinding -- mutable = let x = &mut T; /* no owned pair */
rebindable -- no mutation = let mut x = &T; /* no owned pair */
rebindable -- mutable = let mut x = &mut T; let mut y = T;
If mut would be optional on the let binding, me as the reader would loose the confidence that it refers to the same piece of memory throughout the scope (or up until the first shadowing).
I don't know how often it catches bugs in rust, but I know that I had a "few" accidents where I reassigned/incremented a pointer in C because of missed const at declaration.
I am in favor of downgrading this error to a deny-by-default lint.
mut in bindings strikes me as a misfeature because it can't decide what it is:
It deviates from the more common and much more useful meaning of mut in Rust, which is "exclusive". As others have already mentioned, learning that mut doesn't mean "mutable" but "exclusive" is already an important step for people learning Rust. Except, in bindings, it does mean "mutable", since "exclusive" would be redundant.
C and keyword reuse
This situation is reminiscent of how, in C, static means non-auto, except when auto isn't sensible, in which case it means non-extern. In Rust, mut means exclusive, except when sharedness isn't sensible, in which case it means mutable. In C, auto and extern can't be combined, so they use the same keyword to negate whichever one is applicable. This confuses people learning the language, since static doesn't mean the same thing in all common contexts. I see the same confusion with mut in Rust.
It may be notable that unlike static in C, the two meanings of mut in Rust can be combined, in some cases leading to patterns like &mut mut foo.
Variables declared without mut can't be borrowed exclusively, which breaks even the already complicated mental model above. So there is a sense in which the two muts are connected, since you can't take a &mut reference to a non-mut variable. But under the "&mut means exclusive and pattern mut means mutable" model, there's no good reason for this. So the conclusion is that pattern mut means both mutable and exclusive.
(But even this is subtly misleading, because &mut references can be reborrowed without being marked mut.)
Consider the learner's journey from "mut means mutable" to "mut means exclusive" to "mut sometimes means exclusive and sometimes mutable" and here we are at "mut usually means exclusive but in a pattern it means mutable and exclusive, except &mut T is always exclusive." Can we not at least find a way to simplify that final step, if not eliminate it entirely?
If pattern mut were only a lint against rebinding the name (let foo = 1; foo = 2) it would be reasonable to say that Rust really does have two muts. Would there be interest in downgrading E0596 (cannot borrow `x` as mutable, as it is not declared as mutable) to a deny-by-default lint, while leaving E0384 ( cannot assign twice to immutable variable `x`) unchanged?
I don’t think it’s this bad. For simple cases, say you have a number or a bool, mut means mutable, end of story. There are a lot of simple cases like that.
Also even if your assessment is true, I don’t understand how downgrading the error to a deny-by-default lint would help. Unless you want to encourage people to actually permanently disable this lint and never use let mut. From what I’ve heard above, the proposal to make this a lint is mostly to ease development so you can – temporarily – ignore this error, while you’re focusing on something else. (And as I mentioned above, I still don’t see any issues there that couldn’t be solved by tooling.) Also, I feel like the situation becomes even more complicated for learners:
If it’s currently “mut sometimes means exclusive and sometimes mutable”, then it will/might become “mut sometimes means exclusive and sometimes mutable, except it might not mean anything if you don’t want to worry about it, except people will call you out and bug you if you ever seriously disable that lint, and also while mut on patterns is technically totally irrelevant, don’t you dare assuming that mut on references is irrelevant, too, in the same way, because you’ll get instant UB if you ever try to ignore this kind of mut using unsafe code; also please don’t ask for the 1000th time why #![allow(mutate_immutable_variables)] doesn’t remove your errors about being unable to borrow the target ot &T mutably”
This is a fair point. My preferred solution would be warn-by-default, similar to unused_mut. Still, even from a didactic POV I think there's still something to be said for putting errors in the right category. Pattern mutis effectively a lint: it doesn't break type analysis and leaving it off is not necessarily (or usually, in my experience) a bug. There exist deny-by-default lints that are more serious (overflowing_literals, for example, is always a bug; mutable_transmutes is always a soundness bug). Missing mut cannot cause a bug.
I also don't agree that the option to downgrade a lint makes the situation worse. Again, my preference would be for it to be a warning, but the effect of making it deny-by-default is essentially the same as the current situation, except it leaves open the possibility of converting it to a warning.
In my estimation, that's the situation we are in right now: pattern mut is already technically totally irrelevant, and I don't understand (rhetorically speaking) why the compiler cares so much about it to conflate it with &mut, which is an extremely useful and orthogonal feature that preserves memory safety. Or, at least, if it is important to conflate these two things, why does &mut T get to slip through the DerefMut loophole?
Fair point. Still, learnability doesn’t get any “better”, perhaps unless it’s weaker than deny-by-default. Though – peeking at the thread title again – deny-by-default lint is probably the main point that’s being discussed here.
Still, I’m already sufficiently annoyed e.g. by some people ignoring Rust’s casing conventions; I’m not particularly fond of the idea of some people no longer indicating which of their variables are mutable.
The behavior around &mut T is somewhat special, admitted. An immutable variable containing &mut T still is somewhat different from a mutable one as the variable will always keep pointing to the same location if it’s immutable. Perhaps technically, the main reason why &mut T is the only type that does this is only because function signatures in Rust aren’t able to express this kind of access which is unque-immutable on a shallow level and mutable behind the first dereferencing.
This reminds me of how DerefMut’s API allows implementors to easily mutate some state that’s relevant to the pointer itself, and this defy expectations that the target will stay the same, even without interior mutability. In an alternative world, maybe a Rustlang with a more capable type system might have a DerefMut trait that does allow generalizing what &mut T offers? Probably only opt-in, otherwise Box should arguably be supporting mutation without “mut” right now. It could perhaps also somehow enforce that mutating the pointer itself, or for that matter mutating the target, is not (easily) possible in a DerefMut implementation?
Anyways, at least &mut T is a type that very clearly expresses mutability in its type; maybe the main benefit of having things the way they are right now is that you won’t need to write multiple muts per line for something that would otherwise always need to look like
let mut r = &mut foo…;
On a mostly unrelated note, I do keep liking my proposal from above more and more to relax the rules so that a single, final, mutable use of an immutable variable should be allowed.
IMO, at first brush, this should remain a non-configurable hard error because I don't want collaborating in Rust to involve a million person to person arguments about whether or not we should #![allow(missing_mut)] or whatever. I want it to be easy to read code which does use mutability, which means I want to be able to see, as well as possible, where it is not mutated. I realize internal mutability bends this rule, but it tends to be obvious to me when a data structure pulls this trick, and it does so in a way that is limited. As-is, reasoning about soundness and security in the face of mutation is hard enough. I want less of these arguments, not more. Maybe if we were in a very good position culturally otherwise.
But overall, no one pretends code with atomics or concurrency is easy to read or reason about, but the fact that AtomicInt bends the rules, against both common intuition and common convention, is not a reason to make the entire language as hard to read as code with concurrent data structures. UnsafeCell is an exception in the language for a reason: it's exceptional.
I don't believe the keyword reuse issue is actually as much of a problem as people make it out to be. If someone had a syntax reform proposal (and really, this is almost a different language at this point) which captured all the nuances of, and fully exposed
local immutability
local mutability
uniquely borrowed and mutable
locally mutable or immutable but uniquely borrowable and mutable
shared borrowing and definitely immutable, like actually
shared pointer that is unsafely readable but not writable
shared pointer that is unsafely mutable and isn't exclusive
shared borrowing that is mutable with runtime tricks
with distinctive syntax for each of these that doesn't ever reuse a keyword, I think that would be reasonable, but I don't like these picking-away-at-the-problem-on-the-edges proposals because they seem more likely to remove room to maneuver we might want to leave open to us if we do actually go one step "up". Maybe I am wrong about that, but it seems these higher-level architectural issues are used mostly as points for discussion and not pointers to solutions. And I think as long as we want to talk about keyword reuse, *mut T is up on stage as well.
Overall, to me, we happen to have a word, mut, that, like the word "hot" in English, can mean an actual entropy gradient with something in the environment, or it can mean something is very attention-getting, or it can mean that if you lick it, it will tingle a lot. I think this is actually fine.
It may be too late to change/fix this but the argument that there actually is an issue here is correct. Claiming that mut is useful by some people here is missing the point imo.
Rust has two mental models regarding bindings/variables: the functional immutable-by-default way from its origins in ocaml and the (sound subset of) the procedural interpretation adopted from C. These two models are sort of isomorphic in some sense actually, though the latter is the one that matches the mental model of ownership that is core to idiomatic rust.
Ask yourselves how would you design this had you started from scratch knowing all the lessons learnt from current Rust?
Using Rust's ownership mental model should entail that you are able to mutate anything that is owned. That's the definition of ownership. Furthermore, the C derived conception fits better imo for Rust - it makes more sense conceptually to attach ownership to a place (in memory) rather than a value. A place is an object with operations whereas a value is just one specific value.
Lastly, here's what I mean by isomorphic:
let x = 5;
let x = 6;
Vs.
let mut x = 5;
x = 6;
The first is the functional way - we bind values which are immutable and we can shadow them to "update" the value whereas in the second we do update the place with the new value. Rust allows both (which is a bit redundant) and you can always switch between the two.
The idea to use an immutability "seal" as was mentioned up-thread doesn't really add up imo:
let mut x = ... ;
// Mutate x
let x = x; // locked mutations here?
x = ... ; // Locked! Won't compile
let x = ...; // Nope! Can still "override" to a different value
The point in the example above is that there is no added semantic benefit here, no added enforcement against bugs. You could have just as well added a comment saying "please don't change the value after this line".
Ideally only one of shadowing/mutation should be allowed. I.o.w it is possible to either:
have an owned binding and mutate it as necessary.
Allow to shadow bindings instead of mutating them.
Shadowing /and/ mutating should be a warning as this is the only real scenario that could lead to logic bugs imo.
How? It seems to me that you are missing our point.
The point is exactly that even though it's memory-safe to mutate anything owned, it might still not be what the programmer meant to do. There are plenty of situations in which a value is owned uniquely yet it should be immutable.
This is plainly and factually wrong. Shadowing keeps the original binding and its value alive, it's just not accessible by that name. In contrast, mutation drops the old value and replaces it with the new one. Consequently, the following two code snippets behave differently: the first one compiles, while the second one doesn't.
// This compiles and runs
fn main() {
let v: Vec<usize> = vec![1];
let vp = &v;
let mut v: Vec<usize> = vec![2];
v.push(3);
println!("vp = {:?}", vp);
}
vs.
// This doesn't pass borrowck
fn main() {
let mut v: Vec<usize> = vec![1];
let vp = &v;
v = vec![2];
v.push(3);
println!("vp = {:?}", vp);
}
I am well aware of what shadowing is and I have specifically used the term "isomorphic", not "same".
In case the term wasn't clear, it means there's a mapping to convert from one behaviour to the other as I have clearly demonstrated. In other words, code reading from a binding 'x' in both my examples would see the same value regardless if it comes from a new shadowed binding or from the original one after mutation.
See above.
Also, your examples make my point - Your first example mixes both shadowing and mutation of v and it passes the borrow checker! The point is that the mut keyword there is not part of the ownership model and could (and imo should) have been an orthogonal lint. This is actually the scenario no. 3 where a lint is useful to denote which of the v bindings you've intended to mutate. Mutating the wrong one would have resulted in a logic bug, despite passing the borrow checker. Imagine replacing Vec with some type Foo with interior mutability, for example.
The second example, just demonstrates a borrow error which is off topic. No one's arguing to remove the borrow checker.
This is the crux of the issue. the point I'm making is that this is a separate category of (logic) errors and shouldn't be tied to the definition of a binding. We have other logic errors that are handled by lints and other mesures. Besides Rust already doesn't provide full (and enforceable) immutability anyway.
Others have already mentioned all the existing holes in this argument such as interior mutability.
Here's the thing, when I work on something for a couple days, then I package up a commit and look at the changes again, when I see
- let span_end = states.len();
+ let mut span_end = states.len();
I find it immensely valuable and helpful. Same way in the other direction, the fact that it isn't perfect doesn't matter.
Any argument that is based on the above information not actually being useful to me is very unlikely to convince me, personally, because I know it to be useful.
I'm going to inline some of the points in that blog, because they quite nicely express the issues / the disconnect that some of us feel between mut bindings and &mut references:
the access to the env.errors place is a &uniq kind of access: it can't be &mut since env is not "declared mutable", and it can't be & since we'd then be unable to transitively have &mut access to the errors.
And this is where stuff is inconsistent: &mutmeans&uniq! The difference between the two is then just about circumventing the mut lint or not…
I stumbled upon interesting code in the compiler which special cases an &mut impl FnMut() call (e.g., f()) to actually become (*f)() (i.e., (&mut *f).call_mut(())) rather than (&mut f).call_mut() since the latter would fail if f, itself, weren't declared mut as well.
Another good example is:
Take, for instance, self: Pin<&mut Self>. How many times has any of you stumbled upon a: ah, damn, forgot to add that silly mut to self so that I can perform .as_mut() reborrowing.
See also:
use ::core::ops::IndexMut;
fn foo<const N: usize> (
it: [&mut i32; N],
)
{
*(it[0]) += 42; // OK
*(*it.index_mut(0)) += 42; // Please use `mut it`
}
There are instances where bindings unnecessarily need to be mut
That is a fact, as showcased above (EDIT: a fact that would be even more visible if we did not have language sugar, see what follows).
Some pervasive instances, such as the &mut-reborrow, or the &mut captured by (uniq) reference, have been special-cased by the language so as to drop the requirement, leading to language magic, and thus, inconsistency (see "This hinders abstraction" above from @nikomatsakis).
So, when a compiler diagnostic is not accurate enough not to have false positives, I personally believe it can be quite legitimate to want to be able to, sometimes / in specific scenarios, be able to turn it down: I thus feel sympathetic with the proposal as a whole.
To me it's like unused_must_use, or, even more generally, dead_code: they are very valuable lints when writing and/or reading polished code; but when quickly hacking at stuff, it can be similarly annoying for a missing mut to cause a fatal error. Note that I wouldn't want it completely silenced either: that is, whilst I consider #![warn(missing_mut)] to be nichely idiomatic (for cases such as that one), I wouldn't want #![allow(missing_mut)] among released code.
A possible tangential approach
Note that a tangential approach, for cases such as deref_mut(), or Pin::as_mut(), it could be interesting to convey that some of these APIs could be &uniq-worthy instead: I would love for Pin<&mut T>, for instance, to feature a .reborrow() method that would require &uniq access, whereby performing the same aliasing checks as &mut, but for the needed mut.
I'm not sure that follows? To me this seems more about an odd special casing for arrays. If you use a Vec<&mut i32> instead of [&mut i32; N] you'll have to add that mut again.
So to me the issue with this particular example seems to be that array special case not requiring the mut, not the other way around.
let r: &mut i32 = &mut 42;
let reborrow = &mut *r; // error, can't borrow `r` mutably, use `mut r`
and similarly for indexing, or closures capturing &muts by reference:
let r: &mut i32 = &mut 42;
let mut c = /* move(&mut r) */ || {
let reborrow = &mut *r;
…
};
This leads to:
needing language magic to silence the mut lint in cases involving built-in types ("the &uniq references" for the closure or when reborrowing a &mut or when accessing a &mut field of a structure);
having abstractions cost ergonomics because unable to do that (such as the Pin::as_mut() pain point).
Hence my statement (since I don't count magic as an escalable solution / since any proper solution needs to be compatible with abstraction).
consider case of re-assignment separate from taking a &mut reference. Let's keep let mut for bindings that are re-assigned, but give up on let mut required for taking a &mut reference to a binding.
This will remain a hard error:
let x = 1;
x = 2; // error, can't reassign
But do allow relaxing taking &mut from such binding. This will be pretty close to how const x = 1 behaves in ES6. If you squint hard enough, for interior-ish mutability it's little bit similar to C's shallow const too (in Rust not only UnsafeCell allows interior mutability, but from let's perspective, even the regular &mut T behaves as "interior mutable").
I realize that reassignment case is also rather blurry because of shadowing and mem::replace, but shadowing has to stay, and mem::replace is specialized enough that it doesn't prevent catching simple "I didn't mean to override that value" mistakes.