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

I think this somewhat misrepresents the thing that's being enforced by this rule. It's not about the value but about the variable. Provided its type does not allow interior mutability, you can mostly1 skip sections of code in review and be sure that when an immutable variable is used, it still contains the same value, unmodified, as it did initially. The ability to move out of a variable does not weaken this guarantee in any way. In most cases, such a move its really just an optimization anyways over what would always be allowed if immutability did apply to the value instead of the variable: to clone the value of the variable and modify the clone.

1 The thing you still do need to look out for is new bindings of the same name that could shadow the original one. But these are syntactically easy to spot as only the left-hand-sides of let statements are important as well as the patterns of (if|while) let/match/for/closures that contain the use site of the variable. In particular you can skip looking at any code in between definition and usage that has more indentation than either of those (whereas a &mut v could appear anywhere, even deeply nested in blocks and expressions). Also, with an IDE, when you see an immutable variable being used, you can just jump to definition and immediately be sure that that definition produces exactly the value that's used.


I'm not necessarily against this proposal, but I think there must be some arguments in favor of keeping it a hard error. The total lack of such arguments here seems - to me - as if there's potential to gather more information on the topic first before we can really discuss whether or not it's a good idea. E. g. consider comparing to other programming languages. I'm not too familiar with C-style languages but I assume making a local variable const in any of those (C, C++, Java, etc) might also be triggering more than just a lint of they are modified anyways? Sure, in Rust the default is the other way around, but there might be enough similarities in order to consider prior art when looking for reasons why a hard error might be a good thing.


I personally feel like, from a point of view of pure functional programming, Rust is at an interesting point where it allows mutability, but in a way that - in the absense of interior mutability - is mostly equivalent (ignoring performance benefits of Rust's approach) to the functional style of mutation: take a value by-value and return a new, modified, one. With the significant difference that using mutable references is syntactically a lot neater / more consise. I see no need to make it even more consise by allowing the omission of mut on the variables. I like pure functional programming, because it makes code easy reason about, and with that background I feel like mut annotations are something rather fundamental in the language design. It's perhaps even a bit similar to Rust's approach of making functions signatures mandatory, or the need to annotate the type of a const or a static.

However, with previous experience in Haskell in mind, I just remembered that GHC (the most popular Haskell compiler) has a feature that goes against the fundamental principles of the language: you can defer type errors to run-time, which makes every piece of code (like, every function) that contains type mismatch errors turn into something that will always error at run time, but in turn degrades type errors to just warnings at compile time. The use case: you can still compile your code during development, even if some sections are unfinished, and even test functionality unrelated to the unfinished parts. But you won't be able to disable the type system with this flag, the errors are just deferred to run time.


So going back to Rust, I think there could be approaches that resolve your problems without giving developers an option to outright disable the feature of mutable and immutable variables in Rust. Once we did that, we'd have to forever support this version of Rust where variable mutability is just a weak lint that can easily globally be turned off in a project.

Some (presumably in some ways suboptimal) ideas of alternative approaches follow:

For example it could be made possible to turn this into a warning for cargo check only, if the error being an error is too annoying for fast cycles of modifying code and running check. Or there could be a way to use a flags passed to rustc or an environment variable to downgrade this error, but you wouldn't be allowed to permanently disable the error with something placed into then source code? I think there is more generally sometimes a need to disable even certain warnings (e. g. unused) while your making changes to code that you only want to be turned back on when polishing your code again. Perhaps we could introduce a way to specify a configuration of warnings, and which can be turned on and off easily temporarily and only during development, so that e. g. when publishing or cargo installing a crate or using the crate as an (online/external) dependency, then that configuration mode wouldn't be allowed at all, and certain more radical settings such as disabling the missing mut on variable error (or new options to make tape annotating const items or function return types no longer mandatory) would only be allowed for use in this this temporary in-development mode. We could even keep such new features, like disabling the missing mut on variables error unstable, yet available on the stable compiler, if the design somehow ensures (or at least strongly encourages) that they really are only used temporarily and for convenience of development while code in the process of being changed, and that code that is shared with others or published will never make use of these features. Another feature that comes to mind is trace_macros: It always seems unnecessary overhead to me to be forced to switch to nightly temporarily (with the consequence of needing compile every dependency from scratch a second time) in order to be able to debug a macro, even though nobody cares if the way to debug your macro stays stably the same in the future, as long as it's something that you only ever do manually yourself in the terminal right there and then, and not something that a stable test script depends on or something that needs to work when a crate is used as a dependency.

There's also the alternative of tooling. If your tooling automatically reacts to missing muts and automatically introduces them for you, then this problem shouldn't bother you anymore. Instead of reviewing the changes as they are made (the modus of operation when manually having to fix an error with a suggested change), you could focus on what you focus on, and then review all the automatic implied changes after the fact in version controls. The same kind of mechanism could automatically detect and fix mismatching function return types, or even missing semicolons. Tooling could even be provided by the compiler itself, I'm imagining cargo check with extra flags could accept and automatically modify code with missing mut, missing semicolon or missing or incorrect type signature on const if you want that.

17 Likes