Let’s start the deep dive by looking into a powerful feature of Rust: all variables and references are immutable by default unless qualified with mut
.
To understand why this is important, let’s cover some context first. One of my pet peeves when reviewing C++ code is to ask authors to sprinkle the const
qualifier everywhere: if something ain’t mutated, say so explicitly. This includes marking local variables, function arguments, function return values, class attributes, etc. Unfortunately, there are plenty of people that argue that doing so is stupid: const
doesn’t help the compiler apply optimizations and it makes the code harder to write. So why should everything be marked immutable?
To make the code easier to read. Code is read many more times than it’s written. The easiest code to read is the code that requires keeping the least state in mind: i.e. the code with the smallest “working set” at any point. Reducing the number of mutable variables in a chunk makes its understanding infinitely easier because you know that once a variable has been given a value, it remains that way. You don’t need to carefully look for places where the value might be mutated within the function nor in any of the called functions.
To prevent mistakes when using APIs. Projects grow and it’s hard to track all the interactions between components. You’d pass everything by value, in which case immutability wouldn’t give you anything, but in practice you end up passing lots of values by reference to avoid copy costs. In those cases, it’s very useful to know that calling a specific function won’t mutate its arguments—which, again, reduces the working set you need to keep in mind.
To come up with better designs. Combining the previous two items, thinking of immutability has strong implications in the design of your types and interfaces. The reason I routinely highlight the lack of immutability during code reviews is because, inevitably, adding these qualifiers causes the author to have to redesign the code with immutability in mind. The results are cleaner code that is easier to read and easier to test. See my previous post Readability: Don’t modify variables for some examples.
To help with concurrency. Immutable data structures are trivial to parallelize. Code that deals with immutable data could can be made parallel with ease and, in theory, this could be done automatically.
In the C++ case, I’ve always found it unfortunate that const
is not the default modifier—and we can probably blame historical heritage for that: const
was a late addition to C (first appearing in C89) and C++ had to remain compatible with existing C APIs.
But thankfully, Rust is not bound by such legacy: Rust does differentiate between immutable and mutable data (unlike e.g. Go, which is also a modern language), and has chosen to make the immutable case the default. And, aside from all of the benefits listed above, the borrow checker can take immutability to its advantage and make very powerful sanity-checks about the code. We’ll explore this in the next episode.