Immutable Code with mut self
Recently I started streaming my adventures with godot-rust over at https://www.twitch.tv/paytonrules. Come visit, while I'm still small enough to let you backseat code. The first completed project is a Blackjack game, where the code is entirely written in Rust using the Godot game engine.
I wrote the code bottom-ish up, where I test-drove a command-line version of the app first, and then attached a mostly test-free Godot UI on top. I decided I wanted a Hand type, that would have an add function that returned a new Hand with the new card. It looks like this:
pub fn add(&self, card: Card) -> Self {
let mut new_hand = self.clone();
new_hand.0.push_back(card);
new_hand
}
Experienced Rust programmers may immediately recognize the beginner mistake I made here, but let me explain to you what I was aiming for and then show you the improved solution.
I've spent a long time in functional languages like Clojure and F#, and one thing I've really grown to appreciate in them is immutable collections. Specifically collections that, when you add or remove from the collection, return a new version of the collection - rather than changing the collection in place. This lets you write code like this (in this case using the 'Hand collection').
Hand::new().add(first_card).add(third_card)
Here each add
function returns a new copy of the Hand
collection, and none of them change &self
in their internals. Unfortunately this has the side effect of a clone
, and while Rust has some capabilities for a functional-style of programming it's primary story is around memory usage. In short, if I'm going to copy variables around everywhere I might as well use F#, and have a garbage collector. I'm trading a lot of memory for something I think is …neat. Fortunately there's a better way.
Passing Variables
Rust has four ways of passing variables and keywords for it. The &
operator specifies whether a value is borrowed, and the mut
specifies whether it is mutable or not1, leading to the four combinations. Here's a chart for the possibilites for passing self
:
self | immutable move |
&self | immutable borrow |
&mut self | mutable borrow |
mut self | mutable move |
Now if you're a Go or C/C++ programmer like I have been in the past you might take a quick mental shortcut - "always pass structs
by reference or pointer." This shortcut was particularly natural for me, as I learned it in college 20 years ago and spent the first third of my career writing C++ code. You pass a struct
by reference and the compiler makes a copy for no good reason. So you pass by reference, and when you don't want that reference changed you pass it as a const
.
What does this have to do with the silly code above? Well you'll notice that &self
and its partner &mut self
are in the table as immutable borrows not references. This is a bit of a language cheat, as the term reference is also correct here, and you are passing by reference, but it's more important that you're allowing the receiving function to borrow the variable and the function is in turn promising you'll get the value back. If you use a borrow, then you can use the value again later in the calling scope. Like so:
let (new_deck, first_card) = self.deck.deal()?;
let (new_deck, second_card) = self.deck.deal()?;
The code above uses deck instead of hand, and is logically wrong, but it will compile because the deal()
function borrows the deck. It's still valid if you call it again after the first call, because deal borrows self, and its immutable because it borrows the deck as a &self
. It ALSO requires you to clone the deck at the top of the function, and it does so in a situation where after dealing the card we don't actually care about the original value. We're keeping the original unmodified version around just in case, and making memory allocations, when we don't really need to.
Enter mut self
Let's look again at that old heuristic of mine, "always pass structs by reference". This comes from the world of old-style C where if you didn't pass by reference the compiler would make a copy of the passed in value. So by applying my heuristic I ended up avoiding a copy and the IMMEDIATELY making a clone, which is actually a deep copy. It's arguably worse than the default C/C++ behavior since it pollutes the code too. My clone was necessary because I can't change an immutably borrowed value. However unlike C Rust doesn't make by copy by default, it performs something akin to a C++ move, which means that when passing a value without the borrow operator `&` the called function takes ownership of that value. You can see this rather simply in a compile error, such as the example below:
fn add_shoe(mut shoes: Vec<Shoe>, shoe: Shoe) {
shoes.push(shoe);
}
#[test]
fn can_add_to_mutable_shoe_list() {
let shoes = vec![];
add_shoe(shoes, Shoe { size: 10, style: String::from("sneaker")});
assert_eq!(
shoes,
vec![Shoe { size: 10, style: String::from("sneaker")}]
);
}
The code above creates a vector of shoes, then calls add_shoe
on the list, with a mut
list of shoes, then checks if the shoe was added in the assert_eq!
list. Well it attempts to, because if you execute this you get:
error[E0382]: borrow of moved value: `shoes`
--> src/lib.rs:48:5
|
45 | let shoes = vec![];
| ----- move occurs because `shoes` has type `std::vec::Vec<Shoe>`, which does not implement the `Copy` trait
46 | add_shoe(shoes, Shoe { size: 10, style: String::from("sneaker")});
| ----- value moved here
47 |
48 | / assert_eq!(
49 | | shoes,
50 | | vec![Shoe { size: 10, style: String::from("sneaker")}]
51 | | );
| |______^ value borrowed here after move
The error - borrow of moved value `shoes`
is because the add_shoe
function takes ownership of the Vec<shoe>
. Even though it's mutable, you can only mutate it inside the function. To the outside world, add_shoe
is effectively immutable. It's also useless. Whoops! Well as currently constructed, but what if instead of returning unit add_shoe
returned a new version of the shoes?
fn add_shoe(mut shoes: Vec<Shoe>, shoe: Shoe) -> Vec<Shoe> {
shoes.push(shoe);
shoes
}
#[test]
fn can_add_to_mutable_shoe_list() {
let shoes = vec![];
let new_shoes = add_shoe(shoes, Shoe { size: 10, style: String::from("sneaker")});
assert_eq!(
new_shoes,
vec![Shoe { size: 10, style: String::from("sneaker")}]
);
}
Now I get a new version of shoes new_shoes
where shoes
is unchanged, from the perspective of the caller. Shoes is still unusable, because it's been moved into add_shoe
, and if I want to keep a copy around I have to make that copy myself2. Returning to our original example, the hand of Blackjack I can change the original implementation to also take a mut self
:
pub fn add(mut self, card: Card) -> Self {
self.0.push_back(card);
self
}
In this implementation add
takes ownership, or consumes, self
and then returns it. No copies are made but you still have the same effect, so that the caller can chain calls together.
// Works just fine, returns a hand with two cards
Hand::new().add(first_card).add(third_card)
Now you might ask, what if I want to keep around copy of original value? Well that's on you - clone the value before you pass it along to the function.
let hand = Hand::new();
let original_hand = hand.clone();
let new_hand = hand.add(first_card).add(third_card);
Now this is a little awkward granted, but the contrast to my original solution is that the clone is done deliberately and purposefully by the caller that is using it, as opposed to pointlessly under the hood. It also keeps the more unlikely case, wanting to keep the original value around, from dominating the implementation.
mut self - Move and Modify
So to reiterate, Rust doesn't copy values sent to a function by default, it moves them. This in turn means you can use mut self
as a way to move a value but also cleanly modify it in the original function, and then return self
to support method chaining. It's an efficient little pattern and I wish I had known about it sooner.