Lack of `mut` in bindings as a deny-by-default lint

I agree, and I find that even just the documentation factor of having mut is a big advantage. Anytime something works with many offsets but only manipulates some, for example. It's much easier to spot where the logic is off if there's a mut on something that shouldn't actually change in that scenario.

That helpfulness of course also extends to the last usage if it mutates. An unintended noop is just as wrong as an unintended state manipulation.

So, personally I'd also prefer tooling improvements over making the mut less strict, as it regularly helps me when reading, writing and when I have to diagnose issues after the fact.

2 Likes

The situation you describe has the characteristics

  • You're writing the code under consideration. (You already know how you intend to use the variables; its design is fresh in your mind.)
  • It doesn't compile yet.

I would think that the greatest value of mut annotations is when

  • You are reading (/ reviewing) code you didn't write. (You have theories about what the original author meant, but not certainty. You may also be unfamiliar with libraries it uses.)
  • The code does compile.

That is, correct mut annotations should (in theory) indicate variables that need more careful scrutiny if you don't know what the programmer was thinking. Therefore, their usefulness should be evaluated in review conditions, not only initial-writing conditions.

18 Likes

Well, when Java is mentioned again, perhaps one more interesting note about final on fields: using final on a field ensures that the write to the field happens before any subsequent read of the field after the classe's constructor finishes. In other words, it provides additional memory consistency guarantees for final fields. It may require for the runtime (especially on some platforms) to emit memory barriers to ensure that the ordering rules are obeyed.

From the comments I'm definitely seeing a strong desire for a lack of mut to mean values are immutable. But sadly, it's not what mut does in reality. For immutability of values, mut is a very weak and inconsistent signal.

let x = self.foo();
bar(x);

is it possible to mutate x?

If x was a &mut _ type, then it could have been mutated without a let mut binding. Unique references aren't some edge case in Rust. I probably mutate values through references more often than mut on owned values. And there's of course also interior mutability, moves, and shadowing.

The assumption of "it's a let binding, so the value doesn't change" can be broken relatively easily. It's an incorrect mental model, and it's dangerous to operate on such flawed assumption in code reviews.

4 Likes

To give another example, the following are roughly equivalent:

let x = self.foo();
// ...
let x = self.bar();
let mut x = self.foo();
// ...
x = self.bar();

So, the absense of mut is a rather weak signal when reading the code.

2 Likes

Of course. You can only know a variable is completely immutable of the type isn't one with interior mutability or a mutable reference type. I don't think anyone claimed anything else. Knowing the type of the variable is relevant. But even for mutable references you'll know that the reference itself will stay the same and only its target value can be mutated.

There are key differences. I tried to explain those above (the footnote "1").

A shadowing binding can be introduces in far fewer places syntactically and it can never be hidden deep inside an expression or some block with more indentation than the first variable's definition and the use-case you're looking at. Also if you use tooling to "jump to definition" in the first case you'd jump to the second "let", in the second case you'd jump to the first "let mut".

4 Likes

In terms of teaching, I think Rust has a problem that people understand &/&mut as primarily immutable/mutable rather than as shared/exclusive. IMHO let mut adds to this confusion, because it's even more focused on the mutability aspect.

It obscures the fact that Rust — unlike many other languages with immutability — doesn't have a distinction between mutable and immutable objects, and that objects are always mutable. If you think "but let mut is a kind of mutable-and-owned reference" then it breaks down when you can't have a mut field in structs, and lifetime fallout from having field: &mut makes you give up on Rust.

2 Likes

You don't need to explain these. I think we're all familiar with these features. We don't disagree because of lack of understanding how things work, but on how useful/consistent/reliable this design ends up in practice.

If the argument is that "lack of mut in let helps in code reviews" then my contra-argument is that it's not, because you also need to watch out for types (likely hidden by type inference) and shadowing, so you already need to study the code very carefully to know if it really mutates anything. If you have to study the code very carefully anyway, then let isn't saving you this effort. Lack of mut can also be misleading, and in my judgement a signal that is unreliable is worse than no signal.

2 Likes

Say you have

let x = ...
x.foo();

Can foo mutate x? Currently the answer is no as mut is missing. If mut becomes optional the answer becomes yes. I really value that rust allows code reviews to only inspect the actual code you write most of the time. Making mut optional would require you to look up every called method to see if it has an &mut self parameter even if this method is defined in another file, while currently you only need to inspect the current function to see if a variable can be mutated.

1 Like

If x is a &mut T, then the T can be mutated:

fn main() {
    let x = &mut T::default();
    dbg!(&x);
    x.foo();
    dbg!(&x);
}
[src/main.rs:14] &x = T {
    val: 0,
}
[src/main.rs:16] &x = T {
    val: 1,
}

A binding of &mut _ can be implicitly mutably reborrowed (&mut *x) even if the binding is not mut. For any other &mut-like type, this is not the case; the binding needs to be mut in order to DerefMut. This is what I referred to earlier as the "DerefMut inconsistency."

6 Likes

I don't mind that. That bindings are immutable already gives me advantages. And even for references, there is additional value, even if it isn't perfect. Things usually aren't. Preventing some issues is better than not preventing any.

To be honest, I'm not sure how to even respond in these discussions, where the arguments seem to be that those that say they do get value out of it are somehow simply blind to the truth or something along those lines.

2 Likes

I think that's backwards: the very reason why we need exclusivity is mutability. If no bindings/values were allowed to ever change, there would be no need for a separate mutable reference type, because shared access of immutable data is trivially memory-safe. So yes, mutability is the point.

Or, to put it differently, mutability is a convenience that we would like to sometimes have, and in order to have safe mutability, a second-order consequence is that by the way mutable references must be unique for memory management reasons. So mutability is a higher-level desire, and uniqueness is an implementation detail. It's not the other way around.

3 Likes

I agree that shared access of immutable data is trivially memory-safe. However, Rust doesn't guarantee that shared data is immutable. Shared Rust references allow mutation. The difference is that objects behind shared references need to ensure memory safety in some other way. This is an important distinction!

I see users who are not aware of this difference, and mistakenly think that if something is behind & then it is truly purely immutable, and therefore they have have &mut to change it (yet another example today). This is a gotcha e.g. when they want to implement caching/memoization and write fn get(&mut self, key), because of the need to mutate self.cache. This usually should be fn get(&self, key) with self.cache being a Mutex or some other UnsafeCell cleverness. Interior mutability breaks the mental model of & == immutable.

There are languages without the interior mutability escape hatch where immutable really means totally frozen impossible to change (pure functional languages, as well as Object.freeze in JS). And of course there are lots of languages that don't have any concept of exclusivity and still allow mutation of shared data (not just C, e.g. PHP is a freely-shared-mutable language, and it's even safe, because it's single-threaded and iterators work on copies only).

But back to the topic: even if we say that exclusivity is for mutability. What about let that has exclusive ownership? :slight_smile: It currently tries to make a mutability distinction that is separate from exclusivity.

2 Likes

Surely I can't be the only one that is fine with the status quo?

27 Likes

While interior mutability is a thing, objects commonly present methods as &self or &mut self based on whether they logically mutate the object. (For instance, they might use interior mutability to modify a cache as part of a logically read-only operation.) This is not a universal; for instance, concurrent data structures may allow logical mutation through a &self method. However, it's a reasonably common convention.

So, generally speaking, a non-mut binding like let x = ...; doesn't allow any calls that the structure itself declared as &mut self. I've found that distinction useful, and I appreciate that Rust requires me to add the mut (and that Rust encourages me to drop a mut that becomes unnecessary).

10 Likes

Unless the x is a &mut type itself, and then let x allows calling any &mut self methods. This is not an edge case, e.g. objects can have obj.foo_mut() getters that expose their content. All mutable slices behave that way. You assign them to "immutable" bindings, and then mutate.

This is at best incomplete and inconsistent. For example if mut was meant to allow mutation then this:

let slice = &mut vec[..];
slice.sort();

should be required to be:

let mut slice = &mut vec[..];
slice.sort();

To me logical mutation sounds like a retcon for problems with shared/exclusive being imprecisely called immutable/mutable. Atomic.store exists solely to logically mutate the value, but it's a &self method. Mutex.get_mut exists to avoid locking thanks to uniqueness, and can legitimately be used for reading data.

& is always shared (Copy and Sync), but not always immutable. &mut is always unique, but not always for mutation (and when tangled with a shared lifetime, it can even be unique-immutable (except when shared access could also mutate)).

4 Likes

Consider the following code:

let mut x = Vec::new();
x.push(1);
let x = x;
x.push(2); // This is a compile error

I think above is why Rust has mut in bindings. How would I get an equivalent functionality without mut in bindings?

9 Likes

Yes, in this sense there are two (or possibly more) reasons why one might need immutability, but is that bad? My point is exactly that you might need immutability for logical correctness even if you don't need/want to share the binding. To me, this is precisely why explicit mutability is valuable.

Given that let x where x is of type &mut _ makes trying to tell if a value is mutated by looking at the declaration less accurate, then a lint that encouraged writing it as let x: &mut _ would make that visual check more accurate.

Would such a lint be worth adding?