Some of the first things you learn about in Rust are its ownership and mutability systems. For ownership, you learn about the difference between owned values and borrowed references, where for mutability you learn about immutability by default and how to make a binding mutable. When I learned about this, I took them to be two different permutations that can be applied to data independently of eachother, creating distinct combinations. So since there is mutable and immutable borrowing, there would also be mutable and immutable ownership.
This thinking also lined up with well the syntax in the language, creating a neat and easy to remember symmetry;
like with this function, take
:
Owned | Borrowed | |
---|---|---|
Immutable | fn take(x: MysteryData) |
fn take(x: &MysteryData) |
Mutable | fn take(mut x: MysteryData) |
fn take(x: &mut MysteryData) |
But recently I learned that that’s not actually correct, since being the owner of a binding means you can easily change its mutability state. All it takes is a little shadowing:
let x = MysteryData::new(); // Immutable binding created
let mut x = x; // Immutability is for chumps!
x.mutate(); // Mutation everywhere!
What does this actually mean? Read on!
Confusing, But Not Undefined Behavior
If you’re like me, that code makes you squirm a bit, so let’s make something clear: This is strictly something you can do with owned values, meaning that references still have to obey their original mutability state. Additionally, this also has to obey the rules around which kinds of usages that are allowed to co-exist. Which means that this code is illegal:
let x = MysteryData::new();
let y = &x; // Immutable reference to x
let mut x = x; // ERROR! y is still valid, giving both immutable and mutable use of x
x.mutate();
In other words, this does not impact the safety of a Rust program in the strict sense; it just leaves room for some misunderstandings and confusion.
Mutability Bugs And Where To Find Them
Mutability is a recipe for stepping on your toes, and immutability solves this by turning your toes to stone basically. Okay, so that analogy breaks down pretty quickly, but the concept of immutability fortunately doesn’t; that is, once a value has been declared, it can never change. The reason this is desirable is that it gives you guarantees that no matter how much concurrency you throw at your bindings, no matter what unholy data flows you come up with, your data will always be usable.1
This is also the reason that it feels weird to me that ownership can override immutability; not only does it clash with the intuition on immutability – how can something be immutable if you can just turn the immutability off? – but it feels like it makes logic errors more likely to happen in Rust. There is a reason Rust has a more relaxed definition of immutability compared to languages like e.g. Haskell though.
Since computer memory is inherently mutable – as we would run out memory pretty quickly if it wasn’t – immutability has to be implemented in software. One way to get there is with a compiler that forces you to always create new objects instead of mutating existing ones, but that will quickly eat through your memory. You can still be smart with immutable data and utilize things like persistent data structures, but at the end of the day, pure immutability requires some form of garbage collection to be practical.
This is unacceptable in low level programming, but in the case of Rust, ownership actually brings many of the same benefits that immutability does. After all, if you can determine that nobody else can touch your data, then there’s nobody to screw it up for. So rather than turning your toes to stone, ownership puts a protective bubble around them, which in practice ends up having much the same effect on safety as immutability. The big difference is that ownership can be determined at compile time, making it suitable for low level programming. References still need immutability for guaranteeing safety when they are being shared though, so ownership is not a wholesale replacement for immutability.
Despite ownership being touted a lot for Rust, I had not made this connection with immutability myself, and I only saw it after I watched this talk by Clint Liddick. It’s a good introduction to Rust in its own right too, for anyone curious!
The Four Cases Revisited
Alright, so having established that Rust doesn’t actually need the pure immutability guarantees to achieve safety2, let’s talk about what’s actually going on. We saw that there are four distinct cases in the table in the beginning of the post:
Owned | Borrowed | |
---|---|---|
Immutable | fn take(x: MysteryData) |
fn take(x: &MysteryData) |
Mutable | fn take(mut x: MysteryData) |
fn take(x: &mut MysteryData) |
That feels a bit weird given what we now know about mutability and ownership though. And indeed, this code is completely valid:
fn take(x: mut MysteryData) { /*..*/ } // Function takes "mutable ownership"
let x = MysteryData::new(); // Data is declared immutable
take(x); // Everything is fine apparently!
The reason this works is that there are actually only three distinct cases to look at.
This is because fn take(x: mut MysteryData)
is just syntactic sugar for a function taking ownership and immediatly changing the input to being mutable, like so:
fn take(x: MysteryData) {
let mut x = x;
//...
}
Which means that there’s really no such thing as taking mutable or immutable ownership; there’s only ownership, which gives the power to change the mutability state. In fact, the table could easily look like this:
Owned | Borrowed | |
---|---|---|
Immutable | fn take(x: MysteryData) |
fn take(x: &MysteryData) |
Mutable | fn take(x: &mut MysteryData) |
This also means there’s an obvious behavior we haven’t touched yet: if you can change a binding from immutable to mutable, can you also do the reverse? Yes, and it’s quite handy even! For example:
let mut x = MysteryData::new(); // Create data
x.init_mystery_machine(); // Make some initial changes
let x = x; // Lock it down once we're done
Since owned values have to obey the rules on what simultaneous usages are allowed for a binding, locking a binding down like above gives more freedom to hand out references to that binding once mutation has happened.
The Missing Piece
The final thing to address is whether this can actually be a source of bugs. In my mind it is one of those weird corner cases of the language, where in some big code base, given enough churn and enough different programmers, code like this will come into existence:
let x = MysteryData::new();
//...
let mut x = x;
x.mutate();
let x = x;
//...
Or maybe some newbie will write code like this, thinking they are applying adequate immutability.
I doubt this is something that will happen often, but this code is bad none the less.
x
should be declared mut
from the beginning, to mark that it will be mutated later;
otherwise the next developer reading this will skim the code, assume x
is immutable, and maybe miss that somebody is actually mutating it, creating misunderstandings and, possibly, bugs.
Wrapping Up
Alright, so Rust doesn’t quite have pure immutability, but what does that mean for our code? In truth, not all that much; the code is still safe after all. Where I think it can really mean something though is in the potential confusion people experience when their intution for immutability turns out to be wrong. I know it certainly confused me. So what should we do about it?
The first step is to spread some awareness, and discuss how to actually work with this. My personal stab at such a work guideline would be something like:
- Design your code such that immutable bindings aren’t changed within the current scope
A more extreme guideline could say that once an owned binding has been declared immutable, it should never change again. This is a very hard restriction though, and while you may be able to enforce it in your own code, you can’t force third party crates to do the same. It is after all in the nature of the beast to allow this change, as we have seen that it is still safe to do so.
Instead, I think a better solution is to try and limit the places where this change can occur. That’s why I think it’s okay for the mutability state to change when e.g. an owned immutable binding is passed as input to a function (i.e. into a new scope) that mutates it; the function declaration is a clear boundary that you can look at to see if there is mutation going on.
And while discussing such guidelines to find the best solution is important, I think the best solution is probably with a lint. That’s also why I’m implementing such a lint in clippy at the moment!
That’s All Folks!
Thanks for reading all this! I hope you got something out of it, and do pick up the discussion on how to work with this! I would also love to hear feedback on this post if you have any :)
- Given that the object is successfully constructed at least. This can be an issue in other languages, but Rust actually sidesteps that issue by not having constructors. [return]
- There is also the topic of internal mutability, but it’s not that relevant to my point here, so I’ll skip it this time. [return]