Show warning only when let variable is modified

At some point, I think the noise becomes too much and is the way of writing flow; however, I believe:

It's not noise to me. In the case of match { ... } the bindings stand out and reduce the cognitive noise inside the handling blocks.

could strike a good balance of being DRY but still somewhat explicit. (This assumes that the match binding mode is ref mut for all the things – usually you don’t mix match binding modes that much…)

I do that a lot. That's actually the whole point of my examples. Some things are supposed to be mutated and some aren't. That's valuable information to me.

3 Likes

Sure; but if the bindings modes happen to be uniform (which I believe is the most common case, even if non-uniformity also happens often), then repeating them becomes noise and violates DRY.

Well; We all have different preferences and our brains are not wired the same way. I think Rust should be flexible to accomodate many people's way of thinking. Therefore, I believe more empathy is necessary in language design.

But the question was about examples where this is useful. So I’m not sure what you’re trying to get out of arguing with me here. The whole line of discussion is about showing that having the mut/not-mut distinction wasn’t a mistake and is useful, and even a next generation of Rust would benefit from having it.

2 Likes

Sure; and I agree with this sentiment. My point is that there can be a middle ground between fully explicit and fully implicit.

Where have I argued against that?

I didn't say you had :slight_smile: PS: Let's get on-topic again ^,-

One can do that today by just putting mut everywhere if you don't care, and fixing the warnings.

The thing I don't like about mut-by-default, regardless of how it warns, is that "add the annotations" later means we'd need an annotation like const to mark that intent, which there'd be no warning to help add (unless there was a warning about having neither mut nor const, which sounds terrible). And having done that in C++, I'd much rather do the Rust way of marking mut than marking const.

This is a testable assertion. I bet you could write a compiler plugin or syn script to get data on it; that might be a good way to increase support for your RFC 2401.


Dropping by to add my example of a bug that this just caught:

fn indices() -> Self {
    let mut i = 0;
    ArrayTools::generate(|| { let t = i; t += 1; t })
}

I meant to update i, not t, so the "cannot assign twice to immutable variable" error was great.

2 Likes

Oh sure; I don't hold the "add the annotations" later idea as an absolute. In the case of let mut, I think the current way is right for Rust. We should try to eliminate pain where there is most of it. In the case of let mut I don't feel the pain that much; in the case of match bindings, I do feel the pain a lot.

Sure; just need to find the time to do it :slight_smile: Meanwhile I think @leodasvacas showed that it is sufficiently common.

Most of my points were already made by other people, but I’ll allow myself to reiterate and hopefully summarize for the sake of posterity, especially since more examples of “why having mut is good” were requested.

Personally, I’m not sure why in the year 2018 after decades of existence of functional languages it’s still not accepted as common sense that immutable-by-default bindings have their very pronounced benefits, no matter how “the mut keyword is redundant because compiler always knows if a binding needs to be mutated” or “generated code will be the same anyway” or even “but mut is 3 more symbols to type and learn”.

In my very honest opinion, a programming language, especially one that “helps you write more reliable software”, exists not for the sake of the compiler, and not even for the sake of developer’s typing or even learning convenience, and especially not for getting a juicy share of the Python userbase, so that Rust users number looks more impressive.

It exists so that a developer that finds themselves in actual need to write something more reliable than a Python script can precisely convene their intent to the computer and to other collaborating developers. And the so-called “redundancy” in a language is actually what helps readability and nailing the intent down, including preserving the intent across refactoring and further development.

let a = ...;
let b = ...;
let mut c = ...;

Immediate readability benefit. Now one immediately knows which binding one needs to keep a close eye on. Doesn’t seem worth it? Ok. Let’s add a loop.

let a = ...;
let b = ...;
let mut c = ...;

loop {
	...
}

Now, looking at this code you can immediately know that loop’s state space is merely {all possible values of c}, instead of the much larger {all possible values of a}x{all possible values of b}x{all possible values of c}.

In order to analyze the behavior of the loop only changes to c need to mentally simulated across more than one iteration.

Values of a and b are immediately known at the beginning of every iteration. You know, that there wouldn’t be subtle shenanigans with changes to a and b accumulating in a way that could affect the loop ending condition.

In other words, immutability keeps down the execution course variants you need to consider and keeps the chaos under control.

I disagree with the often voiced argument, that mutability-caused bugs limit themselves to aliasing bugs. Let’s observe how bugs could be easily introduced, if all bindings are implicitly mutable, with the following dumbed-down example.

let a = ...;
let b = ...;
let c = ...;

loop {
	c.do_something();

	// do some other stuff...
 
	if b.check_c_stuff(c) {
		break;
	}

	// still do stuff...
}

The intent was only for b to observe c, doing some complicated calculation based on its own data and the data in c. The data in b is not supposed to change, so naturally it is constructed outside the loop for clarity and performance reasons.

It seems fine and dandy, but then one day someone changes fn check_c_stuff(&self, ...) to fn check_c_stuff(&mut self, ...), because calculating stuff seemed somehow faster while mutating in place.

And everything still compiles. But the loop may now behave in a complicated way, and may even not end at all, due to the fully unintended even though perfectly unaliased mutation that allowed changes to accumulate across iterations and thus corrupt loop end condition logic.

The mut keyword is “inconvenient” for some, because it forces the developer to reconfirm their intent. And I think this is a good thing™, if you do actually want to write reliable software.

Did I actually want changes in b to accumulate? Or do I now want to create a fresh instance of b for every iteration inside the loop? The language asks me that, and when I don’t want my language to ask me that, then I write in Python, not Rust. Which doesn’t mean that Rust is a bad language. (Nor is Python, for the record).

Rust is not Python, and it’s a good thing, because I don’t believe in “one language that rules it all”. I feel the need to point out, that making Rust more “familiar to developers coming from other languages” just to get some more users on board may also take from Rust’s arguably strong points, make it unsuitable for some needs and niches, and make it not that different from hundreds of languages out there. All of which will make it less attractive to learn and write in, in the first place, and thus will ultimately get less users on board in the longer term.

The arguments I also disagree with:

  1. let immutability is a lie, because there is internal mutability.

There are things without internal mutability too, and a lot thereof. A feature doesn’t have to be 100% perfect and watertight to be helpful. If I wanted really watertight, I would have opted for a language with formal verification annotations. Meanwhile, in the absense of formal verification in Rust, let immutability strikes a good balance between easiness of writing code and facilitating writing correct and readable code.

  1. let immutability is a lie, because you can mutate the thing by simply rebinding.

It still provides useful information about how the binding is used in the meanwhile.

  1. let immutability is useless because you can miss where a variable is rebinded or shadowed in between.

The simplest implementation of “Goto definition” in any editor would bring you to the last redefinition point, so you won’t miss anything in between.

  1. let immutability is bothersome because you always have to scroll to the binding and add that “mut” if you forgot it.

Intellij Idea IDE highlights the error at the site of mutable use and allows to fix the binding with the “Quick fix” shortcut without having to scroll anywhere, as well as all such errors in the file or your whole project.

It is my firm opinion that providing users with a solid go-to IDE for Rust and/or rustfix tool should be the way to proceed with ergonomic issues like this one, and should be top priority for Rust anyway, if larger user participation is desired.

  1. let immutability can be made a lint and then people who want the additional nudges from the compiler, will have them, and everyone else can be left in peace.

I’m actually split on this one, but, for example, there is an argument for rustfmt to be less configurable, so that there’s sort of “one Rust style that rules them all”, for the perceived readability benefits for everyone.

Disregarding my personal opinion on the matter, a similar argument could be made that making “mut” keyword optional would detract from code style uniformity. After all, one may not always correctly assess how many people will be reading their code and how much they’ll miss the "mut"s for understanding and preserving original intent, not unlike how unfamiliar formatting style can damage readability too.

One may also argue that in order to write reliable software you need to use reliable components, and avoiding sloppy no-mut style everywhere may provide for just a bit of additional edge.

6 Likes

Because Rust handles safety differently than functional languages. "shared mutable state" is the problem; many languages deal with this through immutability, but Rust does not: it deals with this via sharing. This is true regardless of the keywords involved.

That said, this attitude is why I think we made the right call, even though in some sort of ideal world we wouldn't have to.

The general point of limiting the number of things requiring to track changes of doesn't change.

That's the "all mutability-caused bugs are because of aliasing" argument, which I disagree with.

4 Likes

At the same time, it is exhausting to have people argue ad nauseum that functional programming is more easy to understand and analyze per se. This is not self-evident, it is not something that can be waved around with phrases like “in the year 2018 after decades of existence,” and it is not a helpful argument.

In fact, functional programming is probably not any easier to understand or analyze: https://www.hillelwayne.com/post/theorem-prover-showdown/ It’s a useful tool, but you can write unreadable code under both functional and imperative styles.

So immutable-by-default is a fine default, but it’s not the only important consideration. It’s far from obvious that a language without it would be worse off.

2 Likes

Which is a red herring, because I have not claimed that. I have specifically remarked in the same sentence and clarified a second time too, that I refer to benefits of immutable-by-default bindings, and not functional programming in general.

The link concerns itself with easiness of constructing formal proofs, which is not the same thing as easiness of understanding code at a glance, reasoning about it and preserving intent.

If you have a better source about the latter, than merely asking developers such as myself if immutability-by-default helps them write programs, I would be interested to see it. Otherwise I don't find the argument that dismisses experience gained with languages defaulting to immutability convincing.

2 Likes

I don't dismiss those experiences. They are why I consider immutable-by-default a fine choice. But they are specific to the situations where immutable-by-default helps- there are situations where it does not, and even situations where it gets in the way.

What I'm not convinced of is that immutable-by-default is universally better, or that it's so obviously better in enough situations, that we should start throwing around accusations like "in the year 2018 ... it's still not accepted as common sense that immutable-by-default bindings have their very pronounced benefits." It's not accepted as common sense because it's not common sense.

I think there needs to be more examples of this. At the moment I can't help but have the impression that the only problem with pervasive "mut" keyword could be solved with a good IDE, like it honestly should for any reasonably complex language.

The very link you gave argues that this point of view is quite spread at least, though it poses a valid point that there's no rigorous proof to it.

On the other hand it is hard to provide a rigorous proof or counter-proof for something like this, so it would seem strange if at least non-rigorous testimonies couldn't be accepted as data points in an argument.

And since my own experience also quite agrees with that point of view, I felt myself rather justified to say as much with a sentence starting with "Personally".

2 Likes

We've discussed a few in this thread:

There's the confusion between (im)mutable bindings (im)mutable objects, where people expect mut-less let to prevent the object itself from ever being mutated.

There's the confusion between let mut and &mut, where people don't realize that &mut means a unique reference, and so get stuck trying to use more than one of them when all they really wanted was Cell. (And the related problem where &Cell kinda sucks for more general use because it lacks field projection.)

There's the closely-related confusion between let and &, where people don't realize that & is really a shared reference, and thus wind up resorting to Rc<RefCell<T>> or index-based graphs (and where the language and libraries don't really offer the best alternatives anyway, because everything is written in terms of "interior mutability").

Additionally, the interaction of closures and let bindings is more complex than it appears. I don't recall the details, but closures actually use a third reference type internally, which they wouldn't need if not for let mut. (I think @eddyb has explained this before?)

More generally, quite a few common data structures and algorithms are best (or at least easily) expressed in terms of mutable variables. So when someone goes to implement one of them in Rust, all of the above points conspire to make things more painful than they really need to be.

Now, you could probably fix some of this by renaming the reference types without changing the defaults on bindings. But in some sense, let mut would still be a lie- definitely a useful lie in some situations, but not one without its complications.

Firstly, I want to note that described problems pertain more to the specific Rust keyword choice and implementation of immutable bindings, and not to immutable-by-default bindings in general, which was my point.

Moreover, some of those problems are not immediately relevant to the discussion at hand about optionalizing or removing mut keywords and wouldn’t be solved with it. E.g. even if people expected immutable objects and only got immutable bindings, it’s still the next best thing they could get to protect their objects, compared to removing notion of binding immutability altogether.

I would also disregard the “closure inner implementation complexity” argument. Easiness of writing the compiler should be the least relevant factor in the language design, if I dare selfishly say so, as compiler users are by far more numerous than compiler writers, and they will have to deal with all the quirks stemming from cutting corners in compiler writing.

Similarly I would downplay arguments to beginner confusions. Learning of a language is a one-time experience. Writing in the language may be a lifetime one. At the very least I think that learning curve improvements shouldn’t occur to the detriment of what experienced users could achieve with the language.

When someone implements really complex data structures and algorithms that aren’t already implemented, I would expect them to be already quite proficient in Rust, and then the problem with mut boils down to IDE/rustfix again.

Anyway, thanks for the good summary of the problems. I’m sure it will serve well for future discussion on how to actually improve the situation.

3 Likes

Sure, but only insofar as the complexity doesn't correspond to making the language itself harder to understand. For example, the complexity of NLL is justified because it makes the language easier to understand; the complexity of a secret extra reference type may not be.

This is an interesting argument, because I've also heard the opinion, which looks rather convincing at a glance, that NLL enabled by default may simplify writing code in the more common scenarios, but hinder learning the concept of lifetimes for when one eventually needs to write something more complex, because there's no simple lexical scopes anymore to train on and associate lifetimes with.

Or in other words "one has less fights with the borrow checker, but the fights are now harder", so you could say the learning curve would become even steeper.

But I would define the rule of thumb not as "complexity shouldn't be introduced if it makes the language harder to understand", but further refine "harder to understand" by saying that it's a matter of priority between "this helps learning the language" and "this helps experienced users write code", which is also the thing to consider in the mut keyword case.

Is the potentially steeper learning curve introduced by NLL justified by how it helps development by experienced users who already understand lifetimes?

Is the potentially hard to understand mut keyword justified by how it helps development by experienced users who already understand what it really does and does not?

I would argue that the answer at least to the latter question is "yes", and that any future changes should keep it possible to maintain the distinction between intended and unintended mutability and keep focus on changing things at least as good as immutable let bindings allow it currently.

Just so that everyone is on the same page, I imagine you refer to the &uniq reference briefly described here.

I agree, though I cannot say I remember observing any confusing behavior of closures which can be attributed to having the secret reference type.

As far as I can tell, the most problematic thing about closures is still the notorious problem of lifetime inference at a call site vs moved out to a let binding, which sometimes prevents extracting closures to bindings.

And of course I ultimately recognize the right of the compiler writers to cut any corners they feel necessary, even though in some cases I would find the end result regrettable and it may avert me from the language altogether.

I don't agree with that argument at all. The lexical scopes design was chosen specifically because it was thought it would be easier to learn and teach, but that has turned out not to be the case

People already have an intuitive understanding of control flow (they must, or they wouldn't understand their own programs without borrows), so NLL's brand of error messages that provide an example of control flow where things go wrong is incredibly easy to understand compared to what we have today.

So I don't think this has anything to do with beginners vs experienced users. NLL helps both (or at least that was my intention in using it as an example).

I'm not convinced that this is actually how the mut keyword works. My previous arguments were not about beginners, they were about everyone. Though again, to be clear, I am arguing only that immutable-by-default is not an obvious universal winner, not that it doesn't have benefits.

Also, excellent find on that explanation!

2 Likes