Some Rustaceans go to great lengths to avoid copying data — even once.
This is typically noticeable among those with a background in systems
programming, who are deeply conscious of performance and memory usage, sometimes
to the detriment of code readability. They make heavy use of
mut to mutate
data in place and frequently try to sidestep
.clone() calls at every turn in
an attempt to (prematurely) optimize their code.
I believe this approach is misguided. Immutability — which means once something
is created, it can't be changed — results in code that is easier to
understand, refactor and parallelize and it doesn't have to be slow either.
mut should be used sparingly; preferably only in tight scopes.
This article aims to convince you that embracing immutability is central to writing idiomatic Rust.
Immutability and State
As programmers, we think a lot about state.
The problem is, that humans are pretty bad at keeping track of state transitions and multiple moving parts. (That is, unless you're a chess grandmaster or the parent of a toddler, of course.)
A large fraction of the flaws in software development are due to programmers not fully understanding all the possible states their code may execute in.
— John Carmack
Immutability can help us reduce the cognitive load of keeping track of state. Instead of having to keep track of all the ways in which a variable can change, we know that it can't change at all.
This brings us to Rust, a language that has chosen to prioritize immutability.
Why Did Rust Choose Immutability By Default?
In Rust, variables are immutable by default, which means that once a variable is bound to a value, it cannot be changed (unless you try excruciatingly hard to shoot yourself in the foot).
let x = 42; x = 23; // error: re-assignment of immutable variable `x`
Especially C and C++ programmers tend to be surprised by that design
and their first Rust programs typically contain a lot of
In my opinion, choosing immutability was a good decision, because it helps reduce mental overhead. It is a nod to Rust's functional roots and a consequence of its focus on safety.
If the default was mutability, you'd have to check every function to see if it changes the value of a variable.
Instead, Rust is very explicit about mutability. It makes you write it out every time you create or pass a mutable variable.
Oh, how awfully, painfully explicit!
Warning signs all over the place, which indicate that we are leaking state changes to the outside world.
Compare that to C++:
Here we have to read the function body to see if it modifies our
variable. We need to use the
const keyword in C++ to indicate that a pointer
shouldn't modify the data it points to.
This is somewhat analogous to Rust's immutability, but it is opt-in in C++, meaning you have to remember to use it.
In Rust, explicit mutability is part of the function signature, which makes it easier to understand the implications of that function call at a glance. It even warns you if something is mutable, but needn't be!
warning: variable does not need to be mutable -/main.rs:4:9 | 4 | let mut x = 42; | help: remove this `mut` | = note: ` ` on by default
Rust's immutability-by-default is not just a syntactic choice; it's a deliberate decision to promote code clarity and safety. By requiring explicit mutability, Rust ensures developers are acutely aware of its implications, especially in concurrent programming where mutable states can introduce complexity.
While immutability is often touted for its theoretical advantages, its real-world application can be less straightforward. Let's explore a concrete example to illustrate how an immutable approach can shape our design decisions.
Consider the following (problematic) implementation of a
This is a contrived example and not idiomatic Rust code!
In a real-world scenario, we should use better abstractions, such as a
struct of some sort, which encapsulates the email's content and metadata, but
bear with me for the sake of the argument.
add_email takes a
&mut self, changing both the
The idea here was to optimize for performance by keeping track of the total word
count on insertion, so that we don't have to iterate over all emails every time
we want to get the word count later.
In what may have been a well-intentioned effort to optimize,
total_word_count have become tightly coupled.
We might refactor the code and forget to update the
total_word_count field, causing bugs!
Immutability In Purely Functional Programming
Issues with mutable state are less prevalent in purely functional programming
languages. For example, Haskell doesn't even have mutable variables except when
using the state monad. Therefore,
Mailbox type might look like this in Haskell:
newtype Mailbox = Mailbox [String] addEmail (Mailbox emails) email = Mailbox (email : emails) getWordCount (Mailbox emails) = sum $ map (length . words) emails
This returns a new
Mailbox every time we add a message.
To the keen-eyed systems programmer, this might sound appalling: "A fresh Mailbox for each email? Really?" But before entirely dismissing that idea, remember that in purely functional languages like Haskell, such practices are quite common because they are quite efficient:
- In the
addEmailfunction, you're prepending an email to the list with the
:operator. Prepending to a linked list in Haskell is an
O(1)operation, so it's quite performant.
- While we're returning a new
Mailbox, Haskell's lazy evaluation and the way it handles memory can mitigate some of the potential inefficiencies. For instance, unchanged parts of a data structure might be shared between the old and new versions.
- Linked lists in Haskell are similar to "streams" in other languages, which helps put the performance expectations into perspective.
The functional approach pushed us towards a better design, because it made it
obvious that we don't need the
totalWordCount field at all:
It was much easier to write a version which calculates the sum on the fly
instead of mutating state.
The code is a lot easier to reason about and it might not even be slower. While lazy evaluation has many advantages, its main drawback is that memory usage becomes hard to predict.
Rust's Pragmatic Approach To Mutability
Rust does not have lazy evaluation, in part due to its focus on predictable runtime behavior and its commitment to zero-cost abstractions. Thus, we can't rely on the same optimizations as in languages that support lazy evaluation.
Instead, many Rust developers would probably opt for a mutable approach in this case.
We mutate the original
Mailbox, while now avoiding the
field from the original code. The compiler prevents multiple
mutable references to the same data, making this approach safe.
Our Haskell example wasn't a mere detour; it highlighted how an immutable mindset can often lead to stronger application design, even outside purely functional contexts. By incorporating these principles, Rust guides developers towards better abstractions.
A Word On Performance
In case counting the words becomes expensive, an alternative would be to
refactor the the code such that
add_email takes a
get_word_count method could look like this:
As you can see, we don't need any mutable state to implement the global word count. By using better abstractions, we can achieve equal performance, but better ergonomics.
Move instead of
Lean into Rust's ownership model to avoid mutable state.
It is safe to move variables into functions and structs, so use that to your
advantage. This way you can avoid
mut in many cases and avoid
copies, which is especially important for large data structures.
Don't Be Afraid Of Copying Data.
If you have the choice between a lot of
mut and a few
copying data is not as expensive as you might think.
As computers get more cores and memory becomes cheaper, the benefits of immutability outweigh the costs: especially in distributed systems, synchronization and coordination of mutable data structures is hard and has a runtime cost. Immutability can help you avoid a lot of headaches.
Don't worry about a few
.clone() calls here and there. Instead, write code that is easy to understand and maintain.
The alternative is often to use locks and these have a runtime cost, too. On top of that, they are a common source of deadlocks.
Immutability Is A Great Default
Immutable code is easier to test, parallelize, and reason about. It's also easier to refactor, because you don't have to worry about side effects.
Rust pushes you towards immutability and offers
mut as an opt-in escape hatch
for hot paths and tight loops. Many (perhaps most) other languages do the exact
opposite: they use mutability as the default and require you to consciously
Limit Mutability To Tight Scopes
Good code keeps mutable state short-lived, making it easier to reason about.
The use of
mut should be the exception, not the rule.