Day 3 Preparation:
If you want to see the actual puzzle please sign up to Advent of code.
Having let the “ire’n’whine” settle, cleaned behind my ears, and shaken off the dust from our past battle it’s time to get back to another Advent-ure (b’dum tish) in learning Rust!
These posts have been fun to write, but have been getting… unwieldy. Trying to solve a problem and learn at the same time still feels like the right approach. However, it might be worth just leaning into a pattern that showed up in week one: split puzzle and any relevant sidequests into separate posts. This will let me dive deeply into the Rust specific stuff when I need to without worrying that I’m not directly solving the puzzle. Also, when I do get to solving the puzzles themselves, I don’t need to worry about cluttering those posts with tangents.
Might not be the right thing to do every time, but it feels like the right thing this time. The length of this post will attest to it, I’m sure.
That said, today is about trying to get a better handle on ownership, specifically in the context of methods, which I promise relates to the Day 3 puzzle solve, at least as I see it now. The next post will detail how I integrate what I’ve learned here into actually solving the puzzle.
But first!
House Keeping
Although it was nerve-racking to do so, I posted last week’s write up to Reddit. As expected, the rapids swept it away quickly with very little splash, but one kind soul did offer some helpful input.
glob Wildcards in workspace .toml file
When setting up the aoc_common code I was pretty new to the idea of workspaces and so I had just explicitly put each subfolder in the \[members\] section of the .toml.
This is inefficient in two ways:
- it’s error prone (typos are real)
- it requires that I manually add each puzzle day to the
.toml
Instead, what I can do is use a ‘glob wildcard’ to automatically add any new folders that match a given pattern to the workspace. Like so:
And (and this is very cool) I can add dependencies which will get picked up by everything in the workspace.
As such I can just set up my day_3 folder and get to gettin’.
Now, assuming that I haven’t completely misunderstood all that, I should be able to just get to work. So… yeah… let’s do that.
Puzzle Overview
The puzzle we’re working towards involves helping Santa navigate an endless dystopia of soulless cubes to bring some modicum of joy to the victims of end-stage capi-… to deliver presents.
More specifically we’re tracking how many coordinates on an infinite 2-dimensional grid get hit at least once, knowing that some can get hit more than once. The puzzle input consists of 4 char values:
- ‘^’ = move up
- ‘v’ = move down
- ‘<’ = move left
- ‘>’ = move right
My first thought here was to just build a Struct to hold the position and then throw a HashMap at it to get the puzzle solution. Obviously this is a perfect solution and nothing could possibly go wrong here.
However, it is also an opportunity to dive into methods implemented on Structs, and to play around with ownership. This is honestly something I’ve shied away from in previous Rust runs. Since I want this run to go better and I’ve made the run a goal1 in itself, I have time to swim around in uncertain waters now. To fail safely, so to speak.
So let’s start with the easy win and get the Struct built, as a warm up.
Building the CoOrd Struct
In part one, Santa starts at 0,0 and each char tells him where to go next. Essentially, what we have is an infinite grid of x,y co-ordinates and so our job is twofold:
- write a way to move between points on the grid
- track what points we’ve landed on at least once
As I see it, step one is to build a CoOrd Struct to hold a given position. As per the previous puzzles I’m going to create a coord.rs module, declare it in main.rs and then do the whole TDD thing.
In the snippet above I have
- Written a
test_newtest to let me consider theCoOrd Structand how I might build one. - Defined the
CoOrdwithi32fields, and derived theDebugandPartialEqTraits - Implemented the
new()function that gives me a default startingCoOrdat 0,0
So far, so easy.
If this was a vid’ya game I wouldn’t be getting XP from doing the same things. So let’s not call this a boom (or a comeback). Instead, let’s just embrace our new maturity and move on. Smiling wistfully.
Moving ’round
Ok, so now that we have a CoOrd the next thing we need to do is put together a method which takes the input and then sends Santa on his way.
As far as I can tell there’s a couple of ways I can think about this, mutation or transformation. In essence I can either update/change (mutate) an existing CoOrd, or make a new object from the old (transform). I think that this basically boils down to ownership… but… you guys? I don’t really understand ownership. Let’s see if we can fix that here.
Hold on to your butts.
What’s mine is the borrow checker’s
When defining a method on a Struct the first parameter is always self. This is what allows the method to do something with a particular instance of the Struct (as opposed to associated functions, which don’t operate on a particular instance).
The relevant distinction here though is how the method takes in self as a parameter. As with all things in Rust, we can either borrow or take ownership, and we can do both mutably or immutably. Each of these options implies a different intention.
- Borrowing Immutably: We just want to read the data, and we want the data to stick around after we’ve read it.
- Borrowing Mutably: We want to read the data and do something to it, but we also still want it to be available afterwards.
- Owning Immutably: We want to use the data as it is and then
Dropthe object when we’ve finished with it. - Owning Mutably: We want to be able to change the data as we use it, again letting it
Dropwhen we’re done.
This overview isn’t the whole picture, there’s nuances around how Rust treats the whole ‘pass-by-reference’ vs ‘pass-by-value’ piece. To be honest, I’m taking this post as an opportunity to see this in action and more fully understand it.
With that in mind I’m going to try all 4 approaches, to see what happens, and the hope is that this will let me choose which is ‘correct’ for this kind of puzzle, and learn the principles rather than just the syntax.
Let me just knock up a quick test which can cover the CoOrd Struct and let us get on with it.
All we’re doing here is making a default CoOrd with our new() function, then we’re asserting that calling our change_coord() function will increase the y field by 1. And with that, let’s write up our first shot at the method.
Borrowing Immutably
Just to go over what we’ve got above:
- We’re creating an
implblock on ourCoOrdand defining achange_coordmethod. (yes, it’s not a great name) - the method takes
&selfto immutably borrow the instance ofCoOrdwe’re calling it on. - the second parameter is just whichever
charthat we’re passing in (from the puzzle input) - we match on that
charand use the Struct update syntax to create a newSelfinstance without the need to explicitly set values that are unchanged from the original&self. - the match expression is the last thing within the function so the new
Self {}is what gets returned out of the function.
Let’s see if this passes
Nice, not quite boom-worthy, but I’ll take the W here. I expected this to work because I’ve seen the pattern before in other places. However, I want to check what’s actually happening after I call the method so I’m going to put a little snippet in main.rs and run that to get a better sense of things.
println! debugging in main.rs
The following is a simple script that just lets me see what’s going on at each stage. The reality is I could write tests which each check one of these things, but… well I don’t want to. I need to declare the module in main.rs anyway so I’m going to rationalise my behaviour away.
mod coord; // declaring the coord module
use coord::CoOrd; // bringing the CoOrd into scope
fn main() {
let test = CoOrd::new(); //making a new CoOrd
println!("Original: {:?}", test); // printing it with the Debug syntax
let new_test = test.change_coord('v'); // making a new CoOrd with the method
println!("New from method: {:?}", new_test); // printing the new one
println!("Called the method on Original");
test.change_coord('<'); // does this change in place?
println!("Change Original?: {:?}", test);
println!("New again {:?}", new_test);
}The key thing to pay attention to above is on line 10 where we’re calling the method without assigning it to a variable. I’m checking here to see if this will cause a permanent change to the test CoOrd. In this case I don’t expect it to cause a change or a move error because we’re borrowing immutably. Let’s run it and see!
Ok! So we are able to make a new variable from our change_coord method. But, as we expected, when the method borrows immutably, it has no permanent effect on the instance. I can see where this might be useful, such as in the case where we want to make sure that the initial state is always known, or where we want to make a change that is rollable-backable before we commit to it.
The next thing I want to see is what happens when we take ownership, but keep things immutable. There’s a couple of things I think I need to change.
- Removing the
*deref from theselfcalls inside the match expression - I might need a let statement to have something to return. I really don’t know about this one so I’ll try both
Owning Immutably
The only thing that’s changed above is removing the borrow and therefore the deref so let’s see what wisdom Friend Computer bestows upon us when we run main.rs this time.
error[E0382]: borrow of moved value: `test`
--> day_3/src/main.rs:11:40
|
5 | let test = CoOrd::new(); //making a new CoOrd
| ---- move occurs because `test` has type `CoOrd`, which does not implement the `Copy` trait
...
10 | test.change_coord('<'); // does this change in place?
| ----------------- `test` moved due to this method call
11 | println!("Change Original?: {:?}", test);
| ^^^^ value borrowed here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `day_3` (bin "day_3") due to 2 previous errorsAh, yes… I knew it was comin’. Part of me always knew… but as always Friend Computer is benevolent and kind.
So, what’s happening here is exactly what’s explained in The Book, particularly here:
Having a method that takes ownership of the instance by using just self as the first parameter is rare; this technique is usually used when the method transforms self into something else and you want to prevent the caller from using the original instance after the transformation.
A method that takes ownership of the object will Drop it when the method scope ends. We have two ways to fix this:
- Stop trying to use the object after it’s dropped.
- Follow
Friend Computer'sadvice and deriveCopyandClone.
Let’s just drop the reuse from main.rs first by commenting out the offending lines for now.
Hey, it ain’t earth shattering but it’s honest work.
It seems obvious that deriving the Copy and Clone Traits will allow the code to run without raising a Compilation Error but, it isn’t clear (to me) if we’ll see a persistent change in the Original test CoOrd after we call it without a declaration or shadowing.
// coord.rs
#[derive(Debug, PartialEq, Clone, Copy)] // adding the new Traits
pub struct CoOrd {
x: i32,
y: i32
}
// -- snip --
// -- snip --
// main.rs
fn main() {
let test = CoOrd::new(); //making a new CoOrd
println!("Original: {:?}", test); // printing it with the Debug syntax
let new_test = test.change_coord('v'); // making a new CoOrd with the method
println!("New from method: {:?}", new_test); // printing the new one
println!("Called the method on Original");
test.change_coord('<'); // does this change in place?
println!("Change Original?: {:?}", test);
println!("New again {:?}", new_test);
}It should be obvious that all we’ve done above is a cheeky #[derive] and uncommented the reuse sections of main.rs.
No change.
Without any additional messing around, taking ownership cuts us off from further use of the item. With Clone/Copy we can keep using the original item because those Traits mean that the method is getting a copy of the underlying data. The value isn’t moved into the method.
Speaking of additional messing around though, there is something that I’ve not really considered yet. Namely that we’re not actually doing anything directly to self in the current version of the method.
As I said up above, we’re creating a new object of type Self (which is just a shorthand for “the same type as self”) and returning that out from the method. That’s probably worth taking a look at too, y’know, for the sake of the thing. I’m assuming that we’re not seeing the new Self {} object overwriting the original object because the thing isn’t defined as mutable, and that deriving Copy/Clone is allowing us to bypass a move error but I don’t know that this is the case.
This is where mutation and transformation are most relevant.
Mutation vs Transformation
Honestly these terms feel like a distinction without a difference here, but my understanding is that mutation refers to altering an existing object, whereas transformation refers to changing one instance of a Struct into a new instance. An analogy I’m using is it’s the difference between dyeing a shirt a different colour (mutation) and cutting the shirt up and making a new shirt from the pieces (transformation). So far I’ve been transforming, reading the values from self and using them to populate a new thing. This may mean that my previous implementation was never going to produce a change in the original instance… which makes sense when you put it that way I guess.
I think there may be a few ways that we could do this but Rust does give us the += and -= operators as short hands for updating numerical values, by assigning a new value to that variable. It probably isn’t the same thing as shadowing under the hood but it feels similar from where I am now.
I can see the complexity increasing here as we go forward and keeping all this together in my head is getting tricky so putting some order on it might help. We have three things we’re manipulating at each step and I’m going to put them into a matrix so I can better keep track of them.
| Ownership | Mutability | Operation |
|---|---|---|
| own | mutable | mutate |
| own | mutable | transform |
| own | immutable | mutate |
| own | immutable | transform |
| borrow | mutable | mutate |
| borrow | mutable | transform |
| borrow | immutable | mutate |
| borrow | immutable | transform |
Phew. We now have 8 options to play around with… I wish I’d thought of that earlier, but better late than never I guess.
Just in case anyone is reading this as a tutorial… Don’t. The eight rows above don’t each represent a ‘correct’ option in Rust. They are just the ways I can combine those three different concepts as words. They might all work in different contexts, but just by looking at them, even just as English words, some of them don’t go together.
Get on with it!
Alright! I hear you… and that’s fair. In keeping with our new found systemati… systematiz… steps, I’m going to reorganise things so that we have two different methods.
// -- snip --
pub fn transform_coord(self, direction: char) -> Self {
match direction {
'v' => Self {y: self.y - 1, ..self},
'^' => Self {y: self.y + 1, ..self},
'<' => Self {x: self.x - 1, ..self},
'>' => Self {x: self.x + 1, ..self},
_ => Self {..self}
}
}
pub fn mutate_coord(self, direction: char) -> Self {
match direction {
'v' => self.y -= 1,
'^' => self.y += 1,
'<' => self.x -= 1,
'>' => self.x += 1,
_ => (),
};
self
}
// -- snip --The only change between line 2 and 10 above is that the original method is now called transform_coord. From lines 12 to 21 we’re using the +=/-= syntax to directly alter the fields of self.
I’m now going to write up some tests for these methods, which is the wrong order really, should have written the tests first. This is more like an opportunity to lay out what I think will happen rather than giving me some desired output to iterate towards, however.
// -- snip --
#[test]
fn test_transformation() {
let first_coord = CoOrd::new();
let _second_coord = first_coord.transform_coord('^');
assert_eq!(
CoOrd {x:0, y:1},
first_coord
);
}
#[test]
fn test_mutation() {
let first_coord = CoOrd::new();
let _second_coord = first_coord.mutate_coord('^');
assert_eq!(
CoOrd {x:0, y:1},
first_coord
);
}
// -- snip --This may surprise you, it shocked me when I realised, but we’re still in the “Owning Immutably” section. I know, the last section header feels so long ago.
The good news is that we’re nearly ready to move on, frankly because running the two tests as they currently sit is basically just going to state the obvious… and it was definitely obvious to me before running cargo test, definitely… yep… 100%
This next cell is a long’un. I’ll explain it underneath, really it’s just two errors, but one of them is repeated four times.
--> day_3/src/coord.rs:25:20
|
25 | 'v' => self.y -= 1,
| ^^^^^^^^^^^ cannot assign
|
help: consider changing this to be mutable
|
23 | pub fn mutate_coord(mut self, direction: char) -> Self {
| +++
error[E0594]: cannot assign to `self.y`, as `self` is not declared as mutable
--> day_3/src/coord.rs:26:20
|
26 | '^' => self.y += 1,
| ^^^^^^^^^^^ cannot assign
|
help: consider changing this to be mutable
|
23 | pub fn mutate_coord(mut self, direction: char) -> Self {
| +++
error[E0594]: cannot assign to `self.x`, as `self` is not declared as mutable
--> day_3/src/coord.rs:27:20
|
27 | '<' => self.x -= 1,
| ^^^^^^^^^^^ cannot assign
|
help: consider changing this to be mutable
|
23 | pub fn mutate_coord(mut self, direction: char) -> Self {
| +++
error[E0594]: cannot assign to `self.x`, as `self` is not declared as mutable
--> day_3/src/coord.rs:28:20
|
28 | '>' => self.x += 1,
| ^^^^^^^^^^^ cannot assign
|
help: consider changing this to be mutable
|
23 | pub fn mutate_coord(mut self, direction: char) -> Self {
| +++
error[E0382]: borrow of moved value: `first_coord`
--> day_3/src/coord.rs:54:9
|
52 | let first_coord = CoOrd::new();
| ----------- move occurs because `first_coord` has type `coord::CoOrd`, which does not implement the `Copy` trait
53 | let _second_coord = first_coord.transform_coord('^');
| -------------------- `first_coord` moved due to this method call
54 | / assert_eq!(
55 | | CoOrd {x:0, y:1},
56 | | first_coord
57 | | );
| |_____________^ value borrowed here after move
|
note: `coord::CoOrd::transform_coord` takes ownership of the receiver `self`, which moves `first_coord`
--> day_3/src/coord.rs:13:28
|
13 | pub fn transform_coord(self, direction: char) -> Self {
| ^^^^
error[E0382]: borrow of moved value: `first_coord`
--> day_3/src/coord.rs:64:9
|
62 | let first_coord = CoOrd::new();
| ----------- move occurs because `first_coord` has type `coord::CoOrd`, which does not implement the `Copy` trait
63 | let _second_coord = first_coord.mutate_coord('^');
| ----------------- `first_coord` moved due to this method call
64 | / assert_eq!(
65 | | CoOrd {x:0, y:1},
66 | | first_coord
67 | | );
| |_____________^ value borrowed here after move
|
note: `coord::CoOrd::mutate_coord` takes ownership of the receiver `self`, which moves `first_coord`
--> day_3/src/coord.rs:23:25
|
23 | pub fn mutate_coord(self, direction: char) -> Self {
| ^^^^
Some errors have detailed explanations: E0382, E0594.
For more information about an error, try `rustc --explain E0382`.
error: could not compile `day_3` (bin "day_3" test) due to 6 previous errorsThis seems like a lot, but really it’s just two Rust Rookie Mistakes in one fell swoop. I saw them coming but I’m here to learn not to know and so it feels fair to just run it to see what happens (science is the combination of rational thinking with empiricism after all). Let’s break this down.
Lines 1 to 43 above are basically just repeating the same error four times, and this error actually tells us a lot about the ‘Owning Immutably’ operation that we’re working on. In a twist comparable with The Sixth Sense or The Usual Suspects you can’t mutate something without specifying that it’s mutable (dun dun duuuuuuun). It’s not in the variable declaration or in the method’s signature so the object is just locked. So going back to our matrix above we could add another column indicating that that approach isn’t valid.
| Ownership | Mutability | Operation | Result |
|---|---|---|---|
| own | mutable | mutate | not yet tested |
| own | mutable | transform | not yet tested |
| own | immutable | mutate | no : contradiction (immutable + mutate E0594) |
| own | immutable | transform | valid, but E0382 if reused without Copy |
| borrow | mutable | mutate | not yet tested |
| borrow | mutable | transform | not yet tested |
| borrow | immutable | mutate | no — contradiction (immutable + mutate) |
| borrow | immutable | transform | valid |
I know you knew that but this isn’t about you… Derick.
Anyway…
After this, lines 44 onwards show another error that just confirms what The Book says, if we take ownership with self then trying to use the thing again will cause a move error if the Struct doesn’t implement Copy/Clone2. Which we had already confirmed earlier so at last we can move on to the world of Mutability.
Borrowing Mutably
Having organised the code into a couple of functions with a couple of tests makes it easier to throw things at the wall and see what sticks with less need to clean up between rounds. We just need to add a couple of symbols here and there and we’re away. In both cases here we just need to put &mut before the self parameter and sprinkle some * derefs around.
// -- snip --
pub fn transform_coord(&mut self, direction: char) -> Self {
match direction {
'v' => Self {y: self.y - 1, ..*self},
'^' => Self {y: self.y + 1, ..*self},
'<' => Self {x: self.x - 1, ..*self},
'>' => Self {x: self.x + 1, ..*self},
_ => Self {..*self}
}
}
pub fn mutate_coord(&mut self, direction: char) -> Self {
match direction {
'v' => self.y -= 1,
'^' => self.y += 1,
'<' => self.x -= 1,
'>' => self.x += 1,
_ => (),
};
*self
}
// -- snip --Hopefully at this stage I’ve done enough disclaiming to keep it clear that this isn’t a tutorial, it’s an exploration (or intentional stumble) of a concept. With that caveat clearly stated again let’s run our tests and see what happens.
Compiling day_3 v0.1.0 (/home/day_3)
error[E0507]: cannot move out of `*self` which is behind a mutable reference
--> day_3/src/coord.rs:31:9
|
31 | *self
| ^^^^^ move occurs because `*self` has type `coord::CoOrd`, which does not implement the `Copy` trait
|
note: if `coord::CoOrd` implemented `Clone`, you could clone the value
--> day_3/src/coord.rs:2:1
|
2 | pub struct CoOrd {
| ^^^^^^^^^^^^^^^^ consider implementing `Clone` for this type
...
31 | *self
| ----- you could clone this value
error[E0596]: cannot borrow `first_coord` as mutable, as it is not declared as mutable
--> day_3/src/coord.rs:53:29
|
53 | let _second_coord = first_coord.transform_coord('^');
| ^^^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
52 | let mut first_coord = CoOrd::new();
| +++
error[E0596]: cannot borrow `first_coord` as mutable, as it is not declared as mutable
--> day_3/src/coord.rs:63:29
|
63 | let _second_coord = first_coord.mutate_coord('^');
| ^^^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
62 | let mut first_coord = CoOrd::new();
| +++
Some errors have detailed explanations: E0507, E0596.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `day_3` (bin "day_3" test) due to 3 previous errorsI don’t know about you but I’m sensing a pattern here…
So we have two errors again, one for each version of the method. Let’s work down from the top, because it seems like the errors are also happening in different places during compilation.
mutate_coord(&mut self)
Lines 2 to 16 are telling us something we’ve seen a lot before, that the object is getting moved into the method and can’t be moved back out: E0507, nothing new there. However, I had thought that issue was associated with ownership rather than borrowing. Friend Computer, kind and wise, tells us that Copy/Clone are a quick fix for this, and indeed the Error Index says the same thing, but it also says
Moving a member out of a mutably borrowed struct will also cause E0507 error[…]
So the question I need to think about here is, does dereferencing “move” things out of the mutably borrowed thing? Is it something about the interaction between dereferencing and mutable borrowing that is causing this? Before I move onto the second error I’m just going to drop the offending deref (the one on the return line) and try again.
Nope, we get the mismatched type error. My other thought is to try dereferencing the fields as I updated them, but previously this has given an “i32 can’t be dereferenced” error.
Giz a sec…
Yep error[E0614]: typei32cannot be dereferenced.
Good to know. But this just means I need to go back and read the References and Borrowing section of The Book again…
Man, the goal of one post a week was… ambitious. BRB
“References must always be valid”
Guys… Rust is hard… and… I don’t think it’s all my fault (ImsorrypleasedonthitmeI’mjustsayin’)…
You know what happens when you assume? You’re right some amount of the time, just enough that you think you’ve understood something and then you get kicked in the face by a mule. Let’s break down what I’ve “learned” here3, whether or not I understand it is a different story and I make no promises.
Let’s look at the current method body and then torture some analogies to within an inch of their life.
Here’s what I assumed was happening:
Joan has written a book called “Mutable Reference: Ghost Protocol”. She’s really proud of it and she wants to give it to her editor Mike so he can mark up the book, maybe reorganise some sections, do the editor thing, y’know.
Joan hands the manuscript to Mike, all the while still being the owner of the book, Mike just has it temporarily.
In order to do the editing on the book, Mike takes off the binding and cover and leaves it with Joan, she’ll be putting all the pages back into it when she gets the book back anyway, and she’s using that cover as a placeholder to remind her that Mike has the book.
Mike marks up the text, shuffles some sections around to help with the flow of the story, which alters the book, but at the end of the day it’s the same set of pages, just updated a little bit. He then hands the whole thing back to Joan, who always owned it in the first place, who then puts it back in the same cover she had been using and goes about her merry way.
Alas, this is not the case… Here’s the real story… (I told you I was about to torture some metaphors)
In the version of the method above, Mike is editing the book, but instead of just handing the reordered book back to Joan ready to go into the cover that she’s holding, he’s relabelled it with a new name, “Owned Item: Trixy Hobbitses,” meaning Joan now has an empty book cover and feels that Mike has not only edited the book, but renamed it on top of that.
Joan is very particular about names and feels that Mike has taken an undue liberty with her Chef-d’œuvre. She proceeds to panic and beat him with her no longer valid cover…
See the problem is that Joan never relinquished ownership, just loaned the contents to Mike for him to correct or rearrange. This final change though is too much! It’s just taking liberties!! So the relationship is unrecoverable, and the communication breaks down… and Mike has hospital bills to pay.
Tragedy really.4
This tale of woe is in the service of a technical point that hadn’t landed for me yet: dereferencing is “overloaded” (in both senses of the word) to mean two things at the same time, at least in this context. In MidWit level language, dereferencing means reaching past the reference/borrow to interact with the data it points to; I had assumed that this just let me tinker with the data and be on my merry way. However, it also means that the data get “moved” out of the reference/borrow5 which renders the initial reference invalid. Which can’t work.
Dropping the deref then causes a mismatched types error because &thing isn’t the same as thing.
Thankfully, this has two easy fixes. One of them is the old Copy/Clone derivation which we’ve talked about a lot… Too much really. But the other is to drop the deref and fix the return type.
I haven’t run it yet, but I believe this will solve the first error; I need to solve the other one first. So let’s move onto that.
transform_coord
Quick reminder of the current method
and when we test it we get
error[E0596]: cannot borrow `first_coord` as mutable, as it is not declared as mutable
--> day_3/src/coord.rs:53:29
|
53 | let _second_coord = first_coord.transform_coord('^');
| ^^^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
52 | let mut first_coord = CoOrd::new();
| +++
error[E0596]: cannot borrow `first_coord` as mutable, as it is not declared as mutable
--> day_3/src/coord.rs:63:29
|
63 | let _second_coord = first_coord.mutate_coord('^');
| ^^^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
62 | let mut first_coord = CoOrd::new();
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `day_3` (bin "day_3" test) due to 2 previous errorsIn another post, a less gritty and serious one, with like unicorns and party balloons, this would get a loud chorus of “Rust Rookie Mistake”. But we have our big serious hypothesis testing pants on now so there’ll be none of that.
Derick.
Both of the above errors boil down to one thing; that first_coord isn’t declared as mutable. I’m going to update the tests to pop in a couple of sneaky let muts and rerun the test suite (See? Serious words).
running 3 tests
test coord::tests::test_mutation ... ok
test coord::tests::test_new ... ok
test coord::tests::test_transformation ... FAILED
failures:
---- coord::tests::test_transformation stdout ----
thread 'coord::tests::test_transformation' (9917) panicked at day_3/src/coord.rs:54:9:
assertion `left == right` failed
left: CoOrd { x: 0, y: 1 }
right: CoOrd { x: 0, y: 0 }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
coord::tests::test_transformation
test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00stest_mutation passing is interesting, and I’ll come back to that later. For now, it’s nice to see the tests passing at all, that means the code is compiling and actually getting to the tests. I’m calling that a Pop which is like a Boom only smaller.
Back to the grown up stuff.
The &mut self version of transform_coord is compiling now, which is a win, and there’s some things to pay attention to. Firstly, the object itself has to be declared as mutable in order for the method to be valid, and this will let us transform the original object into a new object, dereferencing with the ‘read past the pointer to the data’ version. Secondly, this isn’t the right method to cause a persistent change in the original object. Which jives with the whole “mutation” vs “transformation” thing that started this epic journey.
This is worth a clear note: &mut self grants permission to mutate the object, but the transform implementation that I’ve written never does that. I read from the borrow but I never write back through it. This means that the signature could also just have &self and the method would do exactly the same thing.
With that in mind then we can fill in two more cells in our matrix
| Ownership | Mutability | Operation | Result |
|---|---|---|---|
| own | mutable | mutate | not yet tested |
| own | mutable | transform | not yet tested |
| own | immutable | mutate | no — contradiction (immutable + mutate E0594) |
| own | immutable | transform | valid, but E0382 if reused without Copy |
| borrow | mutable | mutate | valid — mutates original in place (test_mutation passes)6 |
| borrow | mutable | transform | valid — creates new Self, original unchanged; &mut self doesn’t change transformation behaviour |
| borrow | immutable | mutate | no — contradiction (immutable + mutate) |
| borrow | immutable | transform | valid |
Good, that feels like solid work so far. Some things that I wouldn’t have put together just from reading The Book, which is really the point of this whole endeavour, even if getting there wasn’t the hootenanny I came here lookin’ for.
With that let’s see what fresh hel… what opportunities for learning come from filling in the final two cells.
Owning Mutably
The time has finally come to fill in the top two rows of the matrix. Let’s edit the functions by dropping the & borrows and in the words of a great movie, “get down to business” (don’t @ me). This is probably going to put us back into test failure land because of how they’re written, but we probably know how to fix at least that much.
// -- snip --
pub fn transform_coord(mut self, direction: char) -> Self {
match direction {
'v' => Self {y: self.y - 1, ..self},
'^' => Self {y: self.y + 1, ..self},
'<' => Self {x: self.x - 1, ..self},
'>' => Self {x: self.x + 1, ..self},
_ => Self {..self}
}
}
pub fn mutate_coord(mut self, direction: char) -> Self {
match direction {
'v' => self.y -= 1,
'^' => self.y += 1,
'<' => self.x -= 1,
'>' => self.x += 1,
_ => (),
};
self
}
// -- snip --The methods above have ownership over the object, and so we don’t need to “read through” a reference to access anything. However, the reality is that they are very likely to fail with the current version of the test. At least I’m expecting to end up back in compilation error land.
Compiling day_3 v0.1.0 (/home/sp1d3r-z3r0/MyProjects-tmp/midwitsanonymous/scratch/day_3)
warning: variable does not need to be mutable
--> day_3/src/coord.rs:13:28
|
13 | pub fn transform_coord(mut self, direction: char) -> Self {
| ----^^^^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default
warning: variable does not need to be mutable
--> day_3/src/coord.rs:52:13
|
52 | let mut first_coord = CoOrd::new();
| ----^^^^^^^^^^^
| |
| help: remove this `mut`
error[E0382]: borrow of moved value: `first_coord`
--> day_3/src/coord.rs:54:9
|
52 | let mut first_coord = CoOrd::new();
| --------------- move occurs because `first_coord` has type `coord::CoOrd`, which does not implement the `Copy`
trait
53 | let _second_coord = first_coord.transform_coord('^');
| -------------------- `first_coord` moved due to this method call
54 | / assert_eq!(
55 | | CoOrd {x:0, y:1},
56 | | first_coord
57 | | );
| |_____________^ value borrowed here after move
|
note: `coord::CoOrd::transform_coord` takes ownership of the receiver `self`, which moves `first_coord`
--> day_3/src/coord.rs:13:32
|
13 | pub fn transform_coord(mut self, direction: char) -> Self {
| ^^^^
warning: variable does not need to be mutable
--> day_3/src/coord.rs:62:13
|
62 | let mut first_coord = CoOrd::new();
| ----^^^^^^^^^^^
| |
| help: remove this `mut`
error[E0382]: borrow of moved value: `first_coord`
--> day_3/src/coord.rs:64:9
|
62 | let mut first_coord = CoOrd::new();
| --------------- move occurs because `first_coord` has type `coord::CoOrd`, which does not implement the `Copy`
trait
63 | let _second_coord = first_coord.mutate_coord('^');
| ----------------- `first_coord` moved due to this method call
64 | / assert_eq!(
65 | | CoOrd {x:0, y:1},
66 | | first_coord
67 | | );
| |_____________^ value borrowed here after move
|
note: `coord::CoOrd::mutate_coord` takes ownership of the receiver `self`, which moves `first_coord`
--> day_3/src/coord.rs:23:29
|
23 | pub fn mutate_coord(mut self, direction: char) -> Self {
| ^^^^
For more information about this error, try `rustc --explain E0382`.
warning: `day_3` (bin "day_3" test) generated 3 warnings
error: could not compile `day_3` (bin "day_3" test) due to 2 previous errors; 3 warnings emittedBoom?… being right about being wrong is an interesting experience.
Standing where we are now, upon a new vista of knowledge, the output makes a lot of sense. We have a warning and the same error repeated twice.
The warning is driving home the earlier point that transform_coord doesn’t need mut because it doesn’t try to mut. It reads things, and indeed does use them to make a new thing, but it doesn’t change anything. It’s not that this cell in the matrix is invalid, it’s just that it’s pointless. Kind of a dark metaphor for this site really…
The error, on t’other hand, is just our old friend [E0382] telling us that the object has been moved into the method without Copy/Clone. So the test literally can’t pass because we’re calling first_coord after the move. Let’s, for the sake of completion just implement the traits and try again. My guess is that the warning will still be there, but that the tests will pass.
running 3 tests
test coord::tests::test_mutation ... FAILED
test coord::tests::test_new ... ok
test coord::tests::test_transformation ... FAILED
failures:
---- coord::tests::test_mutation stdout ----
thread 'coord::tests::test_mutation' (90682) panicked at day_3/src/coord.rs:64:9:
assertion `left == right` failed
left: CoOrd { x: 0, y: 1 }
right: CoOrd { x: 0, y: 0 }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
---- coord::tests::test_transformation stdout ----
thread 'coord::tests::test_transformation' (90684) panicked at day_3/src/coord.rs:54:9:
assertion `left == right` failed
left: CoOrd { x: 0, y: 1 }
right: CoOrd { x: 0, y: 0 }
failures:
coord::tests::test_mutation
coord::tests::test_transformation
test result: FAILED. 1 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sI meant run, not pass. You can’t prove I didn’t Derick!
Anyway… as I thought, Copy/Clone lets the code compile. In fairness though, I’m still Schrödinger’s Correct because while test_transformation was going to fail for reasons we covered earlier, I expected test_mutation to pass. The traits are getting us past the compilation error because we’re just copying the CoOrd into the method. If I’m understanding this correctly7 the fact that we’re copying into the method means that the mutation isn’t touching the original. So again, we end up with a pattern that isn’t invalid, just a bit goofy. I don’t know enough to call it an “anti-pattern” but it could be.
So with a whimper rather than a shout we can fill in the last two rows on our matrix
| Ownership | Mutability | Operation | Result |
|---|---|---|---|
| own | mutable | mutate | valid with Copy, but doesn’t mutate the original — mut self consumes a copy8 |
| own | mutable | transform | valid, but mut is unnecessary — the method never writes through self |
| own | immutable | mutate | no — contradiction (immutable + mutate E0594) |
| own | immutable | transform | valid, but E0382 if reused without Copy |
| borrow | mutable | mutate | valid — mutates original in place (test_mutation passes)9 |
| borrow | mutable | transform | valid — creates new Self, original unchanged; &mut self doesn’t change transformation behaviour |
| borrow | immutable | mutate | no — contradiction (immutable + mutate) |
| borrow | immutable | transform | valid |
Phew! We got there.
What have I learned
There’s a few things here that are useful points, and honestly I’ll need to read back over it myself to make sure I’ve grasped it. I’m also going to pass the final table into a cheat sheet page. That page will come in handy in future anyway.
A couple of things stand out though, one practical, one more general.
TDD and hypothesis testing
For those of you who don’t know I’m a psychologist by training who works in the research and human data space (#notclinical). I’m learning Rust because I want to be a better coder, and just as a hobby really, but I ain’t no engineer. This means that I’m going to be bringing some assumptions with me into this process, some good and some bad.
One of the things that this post drove home to me was the… stickiness of using TDD to learn generally about a language rather than specifically about how to get to a specific outcome. ’Round my parts, we call that a construct validity threat (I promise I’ll stop the cowboy thing now).
I was writing tests to see what happens if I manipulated one of my three variables; mutation vs transformation, ownership vs borrowing, mutable vs immutable. Mostly this was fine, but a lot of the time when I was caught off guard, at least earlier on, it was because the code either wasn’t compiling (and so never getting to the test), or because a test could pass even though the thing I was manipulating didn’t really matter.
This was the case with the &self and &mut self in the transform_coord context. Essentially the test as written didn’t really matter because it wasn’t related to the correct construct. Varying the borrow’s mutability wouldn’t have changed the outcome of the test as written so it wasn’t a valid test.
Now I could (and probably will later) have gone through a much longer process of testing every single thing that came up here, and written a much wider suite of tests that would capture panics and a wider range of expected output, but back at the start of this post, I couldn’t have done that. I didn’t know enough about TDD (not that I’m an expert now) or enough about writing methods and how they might behave.
On the one hand I’m glad I know this, on the other hand this is a good data point about TDD for me to consider.
Where I’m at in my journey, I’m “thinking with my hands” so to speak. In a lot of cases I don’t know what the desired outcome is in idiomatic Rust, and so writing a TDD style test in that case may not make sense. The println! debugging in main.rs which I did at the start felt like a way for me to manage that, letting me move onto TDD when I had a better sense of what’s going on.
The benefit of moving over to the TDD approach was that it was easier to make changes quickly.
All this to say that a passing test might have hidden something from me, a method might have passed the test, or failed in a way I thought it might, but unless I knew that I had written a valid test, that could have been hidden. All this is really worth keeping in mind the next time I dive on a Rust related concept rather than just a puzzle solve. It’s not a reason not to use TDD, but it’s not not not a reason to not… you know what I mean.
Updating a thing vs making a new thing
Going over the 8 rows of our matrix above was useful and it let me get a better sense of what the difference is between mutating something and transforming it. It feels less like a distinction without a difference. Without testing it in context I think that the version of the method that works best for solving the actual puzzle is:
It lets me keep a running CoOrd as I move along the input.
There’s a few things worth noting here though:
This implementation, which returns a mutable reference to
Selfwould let me chain other methods in the one call. I could limit this by returning()as well if I didn’t want that to be possible. Coming from Python Polars land, I don’t know why I would want to limit that, but it’s good to know there’s an option.Transformation is an option if I want to leave the original thing untouched. It doesn’t feel like the right move for how I’m thinking about this puzzle, but it is good to know. Only you can tell me if the final code I come up with next week “smells” bad (I prefer Torvalds’ “Taste” analogy but I see code smells come up a lot more online so I’m going with that).
Copy/Cloneexists. However, it feels like an escape hatch that somewhat goes against the language philosophy. If I don’t need a thing then leaving it around by making copies of it feels bad. But theni32implementsCopyand that’s part of how Rust works so who the hell am I to go making value judgements like that?
A MidWit, that’s who
:wqa
Footnotes
As opposed to becoming God’s favourite Rustacean.↩︎
Copy and Clone are different Traits, but every time I’ve tried to derive Copy without Clone it doesn’t work.↩︎
I read through Chapters 4 and 5 of the Book again, and the Error index and then went to my treacherous friend to see if I was understanding any of this. As with all things, it’s up to you if you believe that any of the above content is AI generated. I hope that my voice is strong enough to read as Human, but it may not be.↩︎
This metaphor is a stretch. It doesn’t actually map onto what’s going on under the hood perfectly, but it describes things well enough.↩︎
As I understand it references and borrows aren’t exactly the same thing, but ‘borrowing’ is the act of creating a reference. Rust uses this idea to make safety guarantees that, clearly, I don’t fully parse well. I’m sure this can’t possibly come back to bite me in the future.↩︎
Keep in mind that the return type has to be
&mut Selffor this to work.↩︎Check my username↩︎
This needs
Copy/Cloneto compile, but won’t cause a persistent change in the original object because it doesn’t actually touch the original object.↩︎Keep in mind that the return type has to be
&mut Selffor this to work.↩︎