Is immutability by default worth the hassle?

Any research that shows that it actually prevents bugs or otherwise improves productivity to the extent that it's worth the small bit of extra hassle involved?

I'm curious to hear the thoughts of people who have worked with Rust and other languages to see if they prefer it. Why?

I definitely prefer it, having dabbled in quite some languages. I did definitely notice it preventing some bugs for me, so I'll gladly take the hassle of sometimes fixing a compiler error because I did not declare some binding mutable.

9 Likes

I find it less hassle than the converse since most variables are immutable. Writing in C++ I try to consistently write const on every variable, method, parameter that is immutable, and that is more of a hassle.

23 Likes

Are we talking about bindings here? I'll assume so.

I thought they were great when learning Rust. But after learning enough Rust to understand that &mut is a misnomer and that Rust's ownership and borrow model is about shared vs exclusive and not immutable vs mutable, I became much more ambivalent. I still consider it useful to signal intent, but a lot less critical than I used to.

I'd be okay if it was an adjustable lint, but am also sympathetic to concerns that would just split Rust into competing styles.

7 Likes

The blog post describes an issue and offers three solutions, preferring the first one: drop the idea of immutability of local variables. I prefer the second solution: introduce &uniq references that are still unique, but not mutable.

When comparing Rust and say C++ the answer is clear ´mut´ is one less character to type than ´const´, so immutable by default is less hassle :slight_smile:

Joking aside. Given that a language is going to have mutable and immutable variables it seems better to be to me immutable by default. Because:

As we are talking about the default assumption of the language the question is what happens when you forget or are too lazy to specify mutability in a variable's declaration? There are two cases:

  1. Mutable by default. It often happens that a program is modified by someone other than yourself, in the future, while fixing bugs, adding features or generally refactoring things. That programmer, who may not be familiar with the entire code base, may inadvertently modify a variable not realising it needs to be immutable, thus causing catastrophic failure. Possibly hard to debug intermittent failure that occurs rarely. This is bad. Anyone who has used languages like C/C++ on large code bases with large teams has seen this problem.

  2. Immutable by default. That future programmer makes the same mistake but now the compiler shouts at them. The nasty bug is averted.

Of course that future programmer may well be yourself, months or years after you have forgotten all the details of your project. You will be so happy the compiler shouts at you.

I'm not sure but I suspect assuming immutable by default allows optimisations that the opposite does not. Immutable by default could be a performance gain.

The issue of mutable or immutable by default is one of a dozen defaults that C++ has backwards. Very annoying.

I suspect Microsoft has evidence enough to show that immutable by default is a better choice: They have estimated that 70% of their security issues are traceable to such memory misuse errors: We need a safer systems programming language – Microsoft Security Response Center

In short, by analogy, I would rather my front door is locked by default when I go out and close it behind me.

10 Likes

And keep the mut-or-not bindings? I admit I've never spent a lot of time considering that alternative universe. Does it go like so?

  • &mut coeres to &uniq or &
  • &uniq coerces to &
  • &uniq has the same move semantics and aliasing guarantees as &mut
  • You can create a &uniq from a non-mut variable
  • You can get a &'short mut out of a &'short uniq &'long mut

And then you could write

impl Env<'_> {
    fn helper(&uniq Self) {
    // ...
    *env.errors += 1;
}

fn foo(/* not mut */ env: Env<'_>) {
    env.helper();
}

But helper couldn't replace all of env or get a &mut to any field?

Interesting to think about. I guess it is a more-immutable-reference than &.

That is an interesting perspective.

By contrast, my own experiences, among which programming in Java, have made me come to strongly prefer immutable bindings by default, especially for locals. This preference is amplified by the fact it also is a stronger signal than a lack of final is in Java, since in Rust an immutable binding to a value of a type that doesn't do interior mutability also implies structural immutability.
That is, things can't shift in the guts of such a value as long as that immutable binding exists.
No such guarantee exists at that level in Java, and the general solution to it (immutable types) carries some nonzero performance penalty that may or may not be reasonable to pay.

The signaled intent of a mut is easily worth the couple of chars from my POV.

8 Likes

Don't like the &uniq Self thing.

´uniq´suggests unique which suggests there is only ever one instance of ¨Self¨. It looks ugly and sounds clumsy.

3 Likes

Yes, everything you described sounds right. It separates the concept of uniqueness from the concept of mutability. It would also make &mut no longer a misnomer, because if you don't need mutability you would use &uniq rather than &mut.

This is already something that rustc implements internally to support closures, which the blog post also mentions, it's just that the syntax is not exposed in the rest of the language.

4 Likes

100% of languages, if they live long enoung, finish with proclamation that one should use immutable variables as much as possible. Here are rules for C++, here is Java developers plead it, here we see how JavaScript is moving there…

Why does it happen? Price of error.

If you need something mutable and you declared it as immutable then you get compiler-time error (if we are talking about language with static typing), or easy-to-debug runtime error (if your language uses dynamic typing).

If you think something is immutable, but language doesn't give you a way to ensure it's truly immutable… long hours of one-on-one with debugger, here we come!

Rust is kinda rare (unique?) in that the phase where the fact that you need immutability-by-default, mutability-by-request was realized before language was frozen and thus that rule exist in the language itself and not in the various style guides.

C/C++ are somewhat lucky, BTW: at least C committee added const to it when it wrote first standard, C89. Thus it was only matter of altering style guide.

Many other languages (Go, JavaScript PHP, Python) suffer much more: before they can switch from “this is Sparta spaghetti hairball” to “by default, make objects immutable” mode they have to pass phase where immutability concept is added, in some form, to the language. Python got the appropriate tools in version 3.7, while JavaScript uses some libraries, PHP debates whether to change the language and Go, as usual, tries to ensure that you would have enough rope to hang yourself (but hey, it's simple rope, which should help, I guess).

9 Likes

I don't know if you will find such study, because things like productivity are pretty difficult to objectively measure. In a lab setting you won't have projects large enough and maintained long enough for individual language features to matter, and in the real world you're not going to have comparable projects written in different languages, but by teams of equal skill.

8 Likes

I did. And I don't like it. I don't like &mut and would, probably, prefer something else (&my, &only or &uniq) but I was trying to imagine why would I want that middle possibility (unique reference which doesn't give you the ability to mutate object)… and couldn't. All the examples which I can imagine are extremely convoluted and strange.

Maybe you can show where would you use &uniq is std and why? It's large library, if there are no place where would you apply these… then are they really useful?

Why? On the contrary, this article explains very well why you need immutability by default. I mean: why would you want &x to be easier to write that &my x (&mut x, &own x, or &uniq x).

It's true that marks on local variables (let vs let mut) are not too important. I still prefer let, but not strongly, these are not too much important.

But when you call functions and build larger program from these functions you really want to avoid shared mutable state, if possible, and since &T references can be shared and &mut T can not be shared… &T is better as default.

If you have more than two observers, then it becomes very important that they both “notice” change at the same time.

Thus Rc<T> or Arc<T> or some other form of shared mutability implies that “change ⇨ notification ⇨ action” protocol. Which you need to invent and follow.

But if you have something immutable or if you are the only observer, then there are no need to synchronise changes!

If object is immutable then it doesn't change and we don't have the need to synchronise anything.

And if observer is unique then we have no need to notify anyone because there are no one to notify!

I meant for bindings (as I've assumed that what the OP was talking about, and ala Niko's article), not types (& vs &mut).

Oh. If that's about bindings then I have to disagree with Niko, but not much: these defaults may be a bit more helpful, but solely because you can't apply &mut x if it's not declared as let mut x.

Belt and suspenders, basically.

But I agree that compared to difference between &x and &mut x win (if any) is tiny.

Here is an example from std I could come up with:

let vector = Mutex::new(vec![1, 2, 3]);
// What a strange "mut".
let mut vector_lock = vector.lock().unwrap();
vector_lock.push(4);

With &uniq, that mut would be unnecessary.

But actually, I think the more important reason to have them would be consistency and teachability. Currently people keep talking about how &mut is a misnomer because it's really about uniqueness not mutability which causes endless confusion. So if it was actually made about mutability, it would be easier to understand and it would match the name.

Also it is currently impossible to express what closures do by using regular method call syntax because closures do take &uniq references to local variables. It would be nice to be able to explain what closures do as syntax sugar (you'd also need #[feature(fn_traits)]). When Rust gets a formal specification, those kind of references will have to be in it anyway because of closures, regardless of whether &uniq notation exists or not.

That's what called “strange example”. I tend to consider objects which implement Deref and DerefMut traits as “transpared proxy”.

And then, the fact that mut is needed where you are trying to change “inner object” becomes pretty natural. Even if not, strictly speaking, 100% needed.

I mean: if I would have used AsMut/AsRef traits then it would have been obvious to me what I'm trying to do with inner object, but if it's automatic Deref / DerefMut then where should that mark go?

Place where you put it now doesn't look too onerous to me.

I guess it's more of an example of what Niko article tells: mut in bindings is pretty weak signal. Compiler doesn't need it and sometimes it makes code stanger than without it.

But yeah, I guess in case of smartpointers &uniq would have been useful. Point.

And story with closures is the same, actually: you can only tell closures take &uniq references to variables because of that mut mark in binding.

Again, if you remove it, compiler would know what to do anyway and &uniq references in closures would start behaving like &mut references in other places.

Recall how mut works in more complicated binding, add the fact that let / else is now a thing and you have still more trouble with mut and bindings.

So you're arguing that mut isn't useful in bindings and they should be mutable by default? But you just posted a bunch of links from other languages where they all argue the opposite: that it's important to have immutable local variables. From your first link you posted about C++ Core Guidelines:

for (const int i : c) cout << i << '\n';    // just reading: const

for (int i : c) cout << i << '\n';          // BAD: just reading
1 Like

But it's not required for let x: &mut, so custom DerefMut are the odd one compared to the built-in reference. :slight_smile:

I agree with the opinion in that thread that this is a bug, or at the very least that it's very much an unfortunate corner case that didn't have to exist. (I'd personally prefer if it were just always an error to move from a default-by-ref binding mode to by-copy binding mode.)

3 Likes

I'm saying that while I think immutable by default is useful it's also not universally useful.

And while all popular languages, sooner or later, start recommending it… it's to solve one problem and one problem only: eliminate shared mutable state.

Python, e.g., gives you ways to create immutable objects, but not immutable local variables.

Just read the sentence right above what you copied (emphasis mine):

Reason

Immutable objects are easier to reason about, so make objects non-const only when there is a need to change their value. Prevents accidental or hard-to-notice change of value.

And they even add:

Exception

Function parameters passed by value are rarely mutated, but also rarely declared const. To avoid confusion and lots of false positives, don’t enforce this rule for function parameters.

Rust approach prevents accidental or hard-to-notice change of value pretty decently without mut in bindings. Google C++ Style Guide even says this:

Using const on local variables is neither encouraged nor discouraged.

Immutability is really important for non-local variables. But for local ones it gives pretty weak signal.

It's still useful, in my opinion, but maybe not even “useful enough”.

1 Like