Is immutability by default worth the hassle?

I don't understand how that would be any different for Rust-without-mut-bindings from what it is in C++ without const for local variables:

let mut variable1 = 15;
let variable2 = 16;
// blah blah
loop {
    // blah blah
    variable2 += 2; /// oops, I meant variable1
}

In other words -- having to add the const keyword in a lot of places is so annoying they grudgingly made an exception. Another argument for immutability being the default.

But I do use const for function parameters in my C++ code.

Is that still true? I think that at least in Edition 2021, they reborrow when capturing references, so what would have been a &uniq &mut becomes just a &mut. Let me see if I can scrounge up the proper incantations to demonstrate this...

Here's what I was thinking of (ask for it). Implicit in this RFC I guess, thought it wasn't obvious to me. "Implementation detail" I suppose.

And yes, for at least the simple example

No idea if that covers all cases or if it's back-portable across editions (probably not?), though.

Keep this in your pocket for the next Rust Trivia night, anyway.

3 Likes

Yeah was talking about C's const vs Rust's mut

Still an interesting conversation though

Ha, interesting.

The Rust Reference is still talking about unique immutable borrows, but slapping this #[rustc_capture_analysis] on the example also only shows MutBorrow rather than UniqueImmBorrow.

Well, I could flip the question: is there any research which show that the small bit of extra hassle involved reduces productivity in a measurable way, or increases the bug count? Note that it's generally quite easy to make something mut if you find out you need it: it is a single simple error fix in an IDE, and it's also simple to do manually. But I doubt that any conclusive research exists which solves this question one way or another, for the reasons @kornel mentioned.

But anecdotally, yes, it occasionally catches a bug for me. I try to avoid mut whenever possible, so if I get an unused mut lint warning, it usually means that I forgot to mutate the variable (which could happen e.g. because I have created a new binding, or mutated a shadowing binding in inner scope, instead of mutating the original variable).

Rust provides ample tools to avoid using mutable state (e.g. the collect::<C>() method, iterators, Option combinators etc), so it is generally a non-issue that you have immutability by default. Even in the languages without that kind of support, like Python, I have long learnt to use the Static Single Assignment style of coding, where a variable is never reassigned or shadowed once it's declared. In my experience, it reduced the bug count, because unexpected mutations are damn easy to make and damn hard to find.

That is a commonly repeated misconception (note that the post you cite is from 2014 and is quite outdated). Immutability is part of Rust's memory model, just like aliasing and ownership. It is unconditionally UB to mutate through a &T (unless the mutated memory is inside of UnsafeCell), regardless of its uniqueness. It is similarly unconditionally UB to mutate an immutable local binding. However, that restriction follows from the rule above and the type system, since you can't call ptr::addr_of_mut! on an immutable binding.

Yes, you can always side-step the immutability of a binding by moving it into a new mutable binding, but as far as the language is concerned, this is an entirely different entity.

The difference between mutability and uniqueness is also present in the &uniq unique immutable references used in closures, mentioned in the post. While Niko argued to remove them entirely, they are a part of the current language, so need to be accounted for in any model.

From the user's perspective, it's the mutability that is important and which I want to get, and exclusiveness is just something to endure, because it prevents UB and bugs. I struggle to think where I could want exclusive references, and mutability would be something tagged along which I would have to deal with.

The usual retort is "but UnsafeCell allows shared mutability, so it's sharing which is the real foundation of the language". That's not correct. UnsafeCell can be used to implement shared mutability, but it's more complex than that. UnsafeCell is, basically, "I have this thing by value, but have none of the usual guarantees about it". As people come to realize, even being initialized is not something which can be unconditionally guaranteed under an UnsafeCell (otherwise you get unsoundness). As you know, UnsafeCell also inhibits niches within the contained type, which would lead to unsoundness otherwise.

Finally, even non-aliasing of &mut T is not a given if T: !Unpin. This looks like a hack to me, but that's the world we currently live in. In general, it's certainly not unconditionally unsound, even if dangerous, to have shared mutability in a single thread.


Rust's type system and memory model are way more complex and with more exceptions than a simple slogan can describe, whether it's "references can be shared and exclusive" or "references can be mutable or immutable". "&mut is about non-aliasing" is a useful change of perspective for new rustaceans, and it does make it easier to understand RefCell or File (which allows file mutation through a &). But at the end of the day, neither of those slogans paints a complete or non-contradictory picture, so I see as counterproductive the emphasis some people put on them.

2 Likes

Another point is that immutable variables behave more-or-less like algebraic variables: After you’ve figured out the value once, you can mentally replace every appearance of the variable with that value.

The mut keyword serves as a warning that this analysis does not apply for the tagged variable: To determine its value at some point in the program, you have to consider its entire history instead.

4 Likes

I think you are digging too deep. Actual formal, rules are actually too complex for many to know (and even ones who know them only apply them occasionally, because it's so tedious… most of the time they leave to the compiler the nuiances and only have to think about all the fine details when they use unsafe).

This makes this:

Quite obvious. Yes, you often need or want mutability and Rust gives it to you (but then, Haskell can do that, too), but it's more of a freebee: since you are the only one who touches that variable right now you may play with it freely, just be quiet, Ok?.

Even exception with !Unpin into that category: yes, formally there are two references, we know most of them are harmless (they are inside refences), but since our language model is too primitive to express it we relax &mut into &mut. And similar thing happens when people implement complex data structures manually with unsafe.

You couldn't summarise the whole language reference into one slogan (if that were possible then why would we need that reference) but we can define goals.

And Haskell goal is to make sure there are no mutable state at all (but it gives you anyway if you really need it) while Rust goal is to make sure there are unique borrower for every mutable variable.

Neither achieve their stated goals perfectly and neither makes an ideal (“no shared mutable state ever”), but that's Ok.

If you think about it that 99% of computer use is to manage some kind of shared mutable state (e.g. URLO exist to share various thought with members of URLO and other observers on the internet, banking computer system goal is to keep shared mutable eventually consistent information about our finances and so on).

But because shared mutable state is hard, really hard, always hard… it makes reasoning about your code complex… eventually developers realise that to write realiable code it's good idea to reduce that complexity as much as possible.

You can not remove shared mutable state from the language completely (because then said language would immediately become quite useless for many practical tasks), but you may try to control it.

Haskell and Rust pick radically different roads to the desired goal, it's obvious. But it would be nice if it were possible to actually explan the difference. “Rust cares about uniquiness and not about mutability”.

That's why Rust stopped using unique references with closures: simple observation solved the issue: you don't really need unique references ever. If you arrive at point where you actually need it (like with closures) you can just add one star! And capture &*something instead of &something.

Of course it, simultaneously, added exception for the generators, but, again, an attempt was made to stay without the existing range of types: &x, &mut x, *mut x. And it's kinda obvious that difference between &mut x and *mut x is uniqueness, not mutability.

Thus I have to “agree”:

Yes, that's post is from 2014, it's outdated, but it aged extremely well: today it's even more correct then when it was written.

We no longer use unique references (even implicit ones for closures) and while &mut x turns into *mut x for generators it happens to reduce number of distinct concepts, not because it's, somehow, the best solution.

It's just one of the bad solutions picked as an answer for the problem which have no good solutions.

1 Like

I maintain that references are mainly about shared vs exclusive. These concepts are mixed together, so it's both immutable/mutable and shared/exclusive, but when it needs to be simplified to a single axis, the sharing axis makes Rust make more sense. To me the "& is immutable unless you use UnsafeCell" exception is not merely an edge case, but a very important feature that changes the character of & entirely.

When you see a fn foo(&self) method, that & doesn't always mean it's pure and won't mutate self, but it does always mean it can be shared. And there are methods like get_mut on &mut Mutex that can only be valid with &mut due to its exclusivity guarantee, even if you're not mutating anything. The same method on &Mutex would not be sound, not because of mutability, but because of sharing.

And I think that angle is important for teaching Rust. When people learning Rust implement patterns like caching/memoization they go for fn set(&mut self, because it mutates the cache. Correcting this as "no, you need it immutable, except have "interior mutability" that makes immutable mutable" is odd. But the angle of "a cache needs to be shared, but use a synchronization primitive to make shared mutation safe" makes more sense IMHO.

3 Likes

UnsafeCell is weird. It's basically a "get out of the type system free" hack, because whatever great safety rules you invent, there will always be some sound programs which violate them. In my view, the only reason &mut UnsafeCell<T> isn't "this can be aliased despite normally noalias" is because you can always ssafely cast &mut T to a &T. But the uninitializedness rules I linked above show that the rules are still more complex for different kinds of pointers.

I agree that the "shared vs exclusive" perspective is a useful one to know, and a good one to teach. I object to the common sentiment "mutability is irrelevant, aliasing is", because it's just as much a confusing oversimplification as the converse. And it really grinds my gear when the people repeat "if only we could remove mut keyword and rename &mut to &uniq", because it would just trade one misconception for another, and it devalues the very real benefits of the current mutability system.

1 Like

I agree that single-assignment programming style is useful, but the syntax in Rust for it is not the best.

let mut of bindings is a very different feature than &mut and ref mut. One is a weak lint for variables that doesn't really affect semantics and has loopholes for moves and temporaries, and the other is a hard guarantee that affects values/places. Meanwhile noobs are confused which of the three mut syntaxes they're supposed to use. And if you try const foo = 1 for really-really immutable values in your functions, you're in for a surprise.

It's too late to change it now, but if I had a time machine I'd insist on something like var/val instead of mut in let mut context.

I think this is a misuderstanding.

let x = 13;

is a hard guarantee. It guarantees that x will not change.

You can make a copy and change that, but that's not a loophole to that rule: x will still not change.

The same is true for:

let x = String::from("abc");

You have a hard guarantee: whenever you use x, it will contain "abc".

You can move the string elsewhere and change it there, but then it's no longer x that you're changing. That's not a loophole.

3 Likes

Without following the details of the arguments presented here I have to ask a question:

The question posed in the title of this thread presumes that there is some "hassle" in having to add ´mut´ when we want mutability. As opposed to having to add ´const´ when we don't want mutability, as in C++ for example.

So my question is: What hassle?

Ok, the compiler forces me to add ´mut´ if I try to mutate something and have forgotten the ´mut´. After checking I really want to mutate and can sensibly mutate. It also nudges me to remove ´mut´ if it turns out the the thing is never mutated by my program.

Really, is that it? Is that the hassle?

I would have thought that is so minor a hassle as to hardly worth commenting on.

Or is there more "hassle" about it that I am missing?

5 Likes

Except x, of course, may change, it's just that trying to observe it is UB. Simple example:

   let mut p: *const i32 = &0;
   for i in 0..100 {
        let x = i;
        if i == 42 {
            p = &x;
        }
        if i == 2 * 42 {
           break
        }
   }
   println!("{p}", p = unsafe { &*p })

Here x changes during execution of program and we may observe it if we disable optimizations. Of course, as usual for UB, if we enable these we can get nonsense results, but that different story.

You may pretend that x doesn't change in safe Rust because of soundness pledge but it's different from hard guarantee.

const gives you hard guarantee, let doesn't.

True loophole is use of types with interior mutability. They can be changed without triggering UB.

But even normal ints can be changed, you just couldn't observe it in a correct Rust program.

Both the need to add mut in Rust and the recommendation to use const in C/C++ are hassle.

Only in Rust that hassle is inevitable while in C/C++ you can just pretend const doesn't exist and avoid that hassle.

No that's it. But note that in C++ where that hassle is avoidable and matter of style, not matter of necessity, different style guides recommend different approaches and I haven't seen even a single one which says “use const whenever you can, no excuses”.

If people try to avoid hassle then, to me, it says that hassle is real.

I think that depends on whether you mean the binding x or the String that x holds. We often refer to that String as x, even though it's not very precise. Yet we do it a lot.

1 Like

I wouldn't call what happens in that example "x changes". That program has undefined behavior. Which means the program is meaningless, so you can't meaningfully talk about whether x changes or not.

Yes, I should have qualified "x is guaranteed not to change in a program without UB".

But that's pretty redundant because that's how all guarantees work, including the guarantees that we were comparing let x = to, i.e. guarantees about & and &mut. So in the context of the discussion, this is misplaced off-topic nitpicking, a red herring.

const values also give no guarantees in the presence of UB. It's the same level of guarantee.

2 Likes

The same is true for a moved String as what is true for a copied u32 -- the String after a move can change, just as a u32 after a copy can change.

My point was: there is a real guarantee there that can be precisely stated, so it's not true that the lack of mut is merely a "weak lint" that "doesn't really affect semantics" and "has loopholes".

1 Like

Yes, I agree.

Wait a minute. That ´x´ comes into life at the beginning of the iteration with the let ´x = I;´ Its life ends when the scope ends at the end of the iteration. That is to say, on every iteration a new ´x´ comes into existence and then dies. Every iteration is using a different 'x'.

So I would say the 'x' is never being mutated. It's just that there are many different ´x´s coming and going.

Or am I missing a point?

1 Like

You can. Only to open yourself up to a lot more hassle debugging things when const is not used and someone does not realise they should not mutate something.

Oh good. Hardly a bother then.

Alternatively I could say it shows they are too lazy to think things through properly:)

By analogy I could avoid hassle in life by not buckling up my seat belt, not brushing my teeth, all manner of inconveniences. Provided I don't see or don't care that this greatly increases the probability of bad outcomes.

1 Like