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

I disagree. You would have to show me example code.

If you have let x : Box<_>, then you can only call non-mutating methods on x, if you have let mut x : Box<_> then you can reassign x to some other Box, seems same logic to me.

I think confusion here comes from the fact that &mut has extra meaning, it means "unique pointer" in addition to "pointer to mutable".

Sure, here you go. Only &mut has this "loophole". Note that this is not because dereferencing a &mut T doesn't go through DerefMut; that's also true for Box<T>. The superpower of interest here is that you can reborrow a &mut T without going through &mut &mut T.

Please read the rest of the thread for my and others' rebuttals to this notion.

Well, Box API is such that Box is a container type, so the contents of the Box is part of the state of the Box. Changing the contents of the Box is changing the Box. That it internally uses a pointer is just an implementation detail that is not exposed in the API.

It's the same in C++: if you have a const std::vector<int>, you can't change the int inside, but if you have a const std::unique_ptr<int> then you can change the int "inside".

I disagree that Box is fundamentally different than &mut in that respect. If modifying the contents of a container is modifying the container, but modifying the referent of a (some kind of) pointer is not modifying the pointer, that is a more nuanced version of "mutation" than Rust has the vocabulary to support at the moment. But even so, what about the second example (I edited in), RefMut? Semantically it should be essentially a &mut.

3 Likes

Right, I agree. To me that is a "const-correctness" problem with the API of Deref and DerefMut. The problem is that dereferencing RefMut requires &mut self rather than a weaker and more accurate &uniq self. It would be ideal if Rust had &uniq for unique, non-mutable references, and a corresponding DerefUniq.

2 Likes

As much as I'd like to agree... Those functions have slighted differences, for which I think the mut make sense. The relationship between what you're mutating and when are you allowed to mutate isn't the same for each example.

fn frobnicate(foo: &'a mut Foo) {
  let bar = foo.bar(); // Foo::bar(&'a mut self) -> &'a mut Bar, I presume...
  bar.mutate();        // mutate(&mut self)
}

Since you can't really make a &mut Bar out of thin air (unless leaking a box) I guess you'd have the Bar inside the Foo, which means that 1st Foo::drop will be called then Bar::drop outside of frobnicate().

As for the second one, as I see it's just syntax sugar for

fn frobnicate(foo: Foo) {
  let mut foo = foo;
  ...
}

And as far as semantic changes, the only difference is foo being dropped in frobnicate().

And as for the last one, the mut goes on to bar because you've returned another owned object which has nothing to do with foo (whatever foo was it's either partially or fully moved into the returned Bar). This will change the drop order, no?

But the end result is that the same "if moved then immutable can become mutable, but needs to be explicit" mantra applies to let mut bar = foo.into_bar() and let mut foo = foo.

edit: sent before being finished

This would be nice but either very magical or unclear/incosistant. Would this syntax magic only apply to &mut self methods, or to function arguments as well or ...?

// fn f(foo: &mut Foo) { ... }
// impl Foo { fn g(&mut self) { ... } }
let foo = Foo;
f(foo); // is this allowed, wouldn't this feel even more magical?
foo.g(); // I mean, this is really just Foo::g(&mut Foo)

I mean, if you really want to you can get by not having let mut and just always shadow things with let x = &mut x. Which at least be always saying that if you want to modify memory you need a unique handle, period.

1 Like

Here you're mutating *bar which is part of *foo. You're never mutating foo or bar.

Here you're mutating foo and *bar.

Here you're mutating bar, and foo doesn't even exist at the point of mutation.

All these examples are very logical to me.

3 Likes

Anegdotally, let rather than let mut has definitely saved me from bugs when I incremented a wrong variable or some such.

Regarding the Baby Steps blog post referenced above, it covers this topic well. I much prefer his solution #2 (introduce unique immutable references to the language) to solution #1 (make all variables mutable).

The default-immutable on local variables was one of the things I liked about Rust when I first started learning it.

In C++ it's a major pain point that one has to write "const" everywhere or else your variables are going to be mutable.

6 Likes

I just want to note that nobody is suggesting that we just straight make mut optional on patterns (I think). The benefit of having Rust say "hey I think you meant to put mut there" will still be there, and the "hey you put mut but didn't actually mutate it" nudge as well.

The proposed difference is just that the "you should add mut" becomes a warning rather than an error. This means that it doesn't block compilation, but the compiler still tells you to fix it.

In fact, they're not asking even that -- just that it's made a lint error rather than a hard error, so that individuals can turn the level down for themselves.

Just for a moment for me, imagine if Rust made an unused mut a hard error, rather than a warning. This makes a lot of technically valid programs not compile because the compiler doesn't like them, rather than an actual error in the code.

The lack of pattern mut is (or at least can rationally be viewed as) a very similar kind of "quality" issue, rather than an actual issue in the code. Especially with code-in-motion, where things are changing, you want to know that things are odd, but not have that block testing what you currently have.

It's the same reason that unused variables are a warning (unlike, say, Go, where they are a hard error!); the problem state shows up transitively when code is changing, and while it indicates that the code should be further edited (warranting a lint), it doesn't necessarily mean that the code is too broken to test.

6 Likes

I see declaring an unused variable as analogous to declaring a variable mut and never mutating it. While mutating an immutable variable is analogous to using a variable that is never declared. Should forgetting let x; be a warning?

When I say "let mut", I am saying "I can mutate this variable as many times as I want". 0 times is a valid number of times, so it makes sense that it's a valid program if you never mutate it. Logically I have that right, I don't have to use it, so it's just warning. Whereas the opposite is "I never allowed myself to mutate it, and am still trying to do it", which is not legit.

9 Likes

Let me just add that this warning would be kind of self-contradictory -- if it's legal to not say mut, it means variables are mutable by default, so what is there to "fix" by adding the keyword? Makes no sense to me.

It makes sense to warn that you have code that gives you something that you aren't using, it doesn't make sense to warn that you forgot to give yourself a permission that you already had.

It would effectively split the language into two versions: one with mutable by default variables, and one with immutable by default variables, but with type checking errors stated as a denied warning.

If you were going to say mutating is legal code, I would prefer no warning at all, just declare mut superfluous and not warn about it being missing.

4 Likes

Development is benefited by having a short edit/compile/run/fix cycle, as well as have having nice ways to communicate programmer intention. There are different levels of correctness of code:

  • Code compiles/runs without warnings. That means it's Correct™.
  • Code compiles/runs with warnings. I can see the code's output as well as pointers to potential bugs or areas of vague communication. I should fix those, and I have a lot of information about what to fix.
  • Code fails to compile because of its potential for bugs or areas of vague communication. ← We're here with this particular pattern mut error. We're being nudged that we really should fix these issues, but the code has a clear meaning and we could also have output to inform our fixing.
  • Code fails to compile because it's invalid.

In rust there are some errors which block the displaying of other errors; hopefully this error is not in that category. Ideally the cycle goes 20 errors → fix 20 errors → code works. Sometimes the cycle goes 7 errors → fix 7 errors → 13 errors...

There is a lot of distracting information in this thread, but what is being requested is that in some cases where the program has a clear meaning the compiler should output its warnings (by default errors!), but allow us to run the code so we get even more information.

+1. This seems like the only really good reason for moving this error but it is a straightforwardly good one. I can't see myself ever disabling this lint, but it is just a lint.

5 Likes

If owned variables are mutable by default, wouldn't it make more sense to clarify intent by adding or not adding a nonmut (C++ const) keyword, which would actually make a semantic difference, rather than by adding or not adding a mut keyword without any semantic difference?

If a missing mut warning can be disabled, then almost by definition it's not an "error" to miss a mut, it's a stylistic choice.

2 Likes

Exactly. Stylistic choices may still be encouraged/enforced by the compiler in the form of warn-by-default or deny-by-default lints.

1 Like

There are a lot of deny-by-default lints in that list that say "this was previously accepted but is being phased out". I don't see any that say "this was previously not accepted but is being phased in". If you're going to start allowing something, might as well go all in immediately.

I don't buy the rationale that it's just for temporary development. If it's still supposed to be considered a mistake to have a missing mut, then it seems quick enough to just fix it and rebuild rather than write:

// temporary
#![allow(mut_mistakes)]

If you were going to do that, why not also make other mistakes lints?

// temporary
#![allow(mut_mistakes, missing_semicolons, mismatched_integral_types)]

From this discussion thread, it seems like the real rationale is to allow it permanently as an option because people don't believe that let mut is consistent. But in that case, it doesn't make sense as a warning at all, unless you want to have two very different versions of the language in the wild.

9 Likes

You are not engaging with the arguments being presented. This is a strawman that ignores the distinction between "Code fails to compile because of its potential for bugs or areas of vague communication" and "Code fails to compile because it's invalid" as delineated by @toc in an earlier post. Missing mut in patterns is never ambiguous (missing mut in &mut is); it doesn't require the compiler to make inferences about what you must have meant nor does it make downstream analysis conditional on fixing the "bug". Nobody (in this thread) is suggesting changes like that.

3 Likes

I don't really understand this distinction in this context.

Mutating immutable variables is invalid because of the potential for bugs, and in order to enforce better communication of intent.

Similarly, adding u32 + u8 is invalid because of the potential for bugs and in order to enforce better communication of intent.

Same thing for semicolons, missing semicolons are invalid because of the potential for bugs and in order to enforce better communication of intent.

All three examples could be made to compile with relatively small changes to the design choices made by the language, but there are good reasons these choices were made.

So I don't really understand what you guys are trying to say with this distinction.

The issue isn't so much how it would affect compiler developers, I was talking about how it would affect users.

2 Likes

I agree with tczajka: while these may seem less intuitive to allow, the compiler could be altered to permit them. There is not a difference.

I understand the arguments that describe a model where Rust's current behavior can be characterized as "just a lint that happens to be deny by default". However, these hinge on a design-level view which abstracts the code purely into compiler commands or ownership control, as opposed to a method of communicating a program, including to human readers. And "immutability by default" is a deliberate element of the language's design. I don't have to hypothesize that, you can see it in things like Graydon's old slides, which mention "immutability by default" twice. http://venge.net/graydon/talks/intro-talk-2.pdf

It may be inconsistent in some ways, which is why I would be supportive of a more comprehensive reform, but it is not "just a lint". To change what let means directly alters what is meant by and communicated by declaring a let-bound variable, which means it does, literally, alter the language's semantics, even if the execution happens to follow similar paths. Saying "if we changed the rule about how something is meant to be read, we could change this error to being allowed in some cases" is, quite literally, making a statement about code validity.

A change that may require less work in the compiler than the examples given by tczajka: downgrade unsafe to deny-by-default. It is, after all, "just" a lint... yet is also a fundamental design feature as well, because it communicates certain things about the program, and we do not actually allow people to circumvent it.

9 Likes

This is the whole disagreement. Nobody here is arguing for changing what is meant or communicated by let. let would continue to mean "immutable binding," and any warnings should be addressed before committing. In fact the original proposal is for a deny-by-default lint, so without opting into a lower lint level the developer experience wouldn't even change.

The idea is rather to change how the tooling reacts to these kinds of contradictions in meaning. These contradictions are of course extremely useful to note and fix, but they are not fundamentally an obstacle to running a partially-correct program, and running a partially-correct program is quite useful: that is the state of the program for the vast majority of its development time!

The motivation is the same as the motivation for todo!(), or letting the error message for fn f() -> _ suggest an inferred return type, or letting programs with type errors run (and error at runtime). It's another useful tool for inspecting and analyzing programs.

This is putting the cart before the horse. Of course that is how it works today, but how important is it? For example, other languages with the same sort of design principles as Rust often don't have unsafe blocks, they just put the string "unsafe" in the name of unsafe operations.

A much more salient comparison here would consider the motivation for making something a lint. let mut is, at least for some people, quite a common speed bump while editing code. Are missing unsafe blocks similar? I don't believe so, personally- but IMO this is how we should be making these decisions, not just using the status quo as a hammer.

1 Like

I don't think the first two things you list here are in the same category as allowing a missing mut on let. You cannot leave out a todo!() (in a function with non-() return value); you have to actually put it there in order for the program to compile. Similarly, you cannot leave the _ return type, you actually have to fill in the inferred return type in order for the program to compile. Similarly, missing a mut on a let gives you an actionable suggestion of a small change, but you need to make that change in order for the program to compile.

The last thing is not a capability of the Rust compiler, as far as I'm aware.