Day 3 Solve.

rust
advent-of-code
learning-out-loud
not-a-tutorial
Poor guy dudn’ know if he’s comin’ or going’.
Author

The MidWit

Published

August 9, 2026

Note

If you want to see the actual puzzle please sign up to Advent of code. They’re great puzzles, and a really good way to learn the basics of a language.

If you want to skip directly to my solution to the puzzle you can jump to here.

Welcome back intrepid wanderer! Come on in, the fire’s lit, the kettle’s on, and the MidWit is ready to stumble through another puzzle.

Having spent the last post sharpening my axe on the whetstone of ownership (the “whetstownership”, if you will), the time has come to make like social-media and track some people’s movements.

A quick recap is probably in order.

What are we even doing?

The aim of this puzzle is to take some input, use it to manipulate a set of co-ordinates, and keep track of how many individual points, on an infinite grid, we alight upon as we go. As detailed (among other things) in my previous post, my plan is to store the current co-ordinate in a Struct and use a method implemented on that Struct to move around.

Having worked through different options for how I might do this, and received some really insightful input from some kind souls on Reddit, it seems like mutation is the move (b’dum tish) here.

So here’s the version of coord.rs that I’m bringing with me into this puzzle:

coord.rs overview

use std::error::Error;
use std::fmt;

#[derive(Debug, PartialEq)]
pub struct CoOrd {
    x: i32,
    y: i32
}

#[derive(Debug, PartialEq)]
pub enum Direction{
    North, 
    South,
    East,
    West,
}

#[derive(Debug, PartialEq)]
pub enum DirectionError {
    BadInput,
}

impl CoOrd {
    pub fn new() -> CoOrd {
        CoOrd {x:0 , y:0}
    }


    pub fn mutate_coord(&mut self, direction: Direction) -> &mut Self {
        match direction {
            Direction::South => self.y -= 1, 
            Direction::North => self.y += 1, 
            Direction::West => self.x -= 1, 
            Direction::East => self.x += 1, 
        };
        self
    }

}

impl fmt::Display for DirectionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DirectionError::BadInput => write!(f, "bad input char"),
        }
    }
}

impl Error for DirectionError{}

impl Direction {
    pub fn read_direction(input:char) -> Result<Direction, DirectionError> {
        match input {
            'v' => Ok(Direction::South), 
            '^' => Ok(Direction::North), 
            '<' => Ok(Direction::West), 
            '>' => Ok(Direction::East), 
            _ => Err(DirectionError::BadInput), 
        }
    }
}

The eagle-eyed amongst you will notice that this is quite different from what we ended up with when we finished the deep dive into ownership and mutation. This is a combination of some of that generous Reddit advice I mentioned above, and some techniques I played around way back when I wrote the aoc_common module back on day 11.

If you want the full epic tale of how we got here please see the previous post but the new code starts on line 10. We have two new enums2. Direction holds 4 variants to allow for more precision when handling the input to Santa, I mean, we’d hate to accidentally cause the poor guy to clip between rooms and get stuck! This is accompanied on line 18 with a DirectionError which is really just there to let me plan for the case where we could get an illegal char, taking advantage of the Result ergonomics.

We’ve taken the mutate_coord method from our last adventure and we’ve updated it to use Direction rather than just raw text input. This allows us to mutate a single CoOrd instance and keep working on it rather than making a new one at every step. We’ve also dropped the transform_coord method entirely, to keep the file as clean as possible.

As for the CoOrd Struct, so far I’ve only derived Debug and PartialEq on it. I was reliably informed that it may make sense to add Copy and Clone to it, but I’ve decided to leave them off unless (and until) Friend Computer tells me I’ve made some grave and unforgivable error.

I’m not claiming any of this is perf… good, and if I was just trying to wordle this MFer then I wouldn’t need a lot of the Direction and DirectionError code. However, the aim here is to lean into “polished” rather than just “finished”. To get the reps in and practice reaching for the tools that Rust gives me rather than trying to work around or skip them.

As an aside, I get that these days a lot of the more boilery-platery code can (and maybe should?) be written by LLMs, but I want to understand it myself before I give in to the vibes. It’s just one midwit’s opinion but learning should be the goal, not just shipping lines of text.

Anywho, that’s what I brung to this party, warts and all, so let’s see if my thought of “Throw a HashMap at it” will actually let me solve the puzzle.

Part 1

Man, that was a lot of words just to get to some “novel” code 3 but let’s dive into main.rs and see how we get on.

mod coord; 

use std::collections::HashMap;
use std::error::Error;
use aoc_common::read_challenge_input;
use crate::coord::{CoOrd, Direction};

fn main() -> Result<(), Box<dyn Error>>{
    if let Ok(text) = read_challenge_input("./input.txt") {
        println!("There's some text!")
    }
    Ok(())
}

This in no way resembles the final implementation but I’m just trying to eek out an early win here, feel some sense of mastery and get a little “spaced repetition” in… It’s a psychology thing, really complex, no need to bore you with the science of it now… just trust me (Fredrickson 2001; Bandura and Schunk 1981; Amabile and Kramer 2011; Cepeda et al. 2006) 4.

Up on line 1 we’re just declaring our coord.rs module so that it’s part of the program, then from lines 3 to 6 we’re bringing things I know I need into scope with the use declarations. Right now I don’t believe I need anything else in scope, but, as is the theme on this general adventure, I don’t know what I don’t know.

Starting on line 8 I’m opening the main function and then I’m doing something I haven’t done before: specifying that main returns a Result instead of nothing.This was pretty new to me, but it’s cool. The fact that we can use a Box struct to ‘accept’ anything that implements the Error trait is really nice.

By way of explaining this to myself, what this does is it allows us to say “I don’t know how big this will be at compile time, but I do know how big a pointer is, and so so I’m going to make a Box of a known size that points to some instance of a concrete type”. The addition of dyn Trait adds even more flexibility by letting us add “I don’t even know what concrete type of object this will be, but I can build to box to accept things that implement a given trait”.

Find.

This.

Hilarious.

It’s like Friend Computer asked you “What do you think you’re doing?”, you respond

“Jeez boss, I’ve no clue. This could be anything, but if it makes you feel better, I’ll slap together a really weird box for it”

and they answer

Hysterical.

Anyway, all that up above really just let’s me read in the text input from the puzzle in a way where I can spot an error (for example if I point the code at the wrong file location). When we cargo run we get some pretty expected output that mainly warns me that there’s a lot of unused code, but that the input is found. Rather than spending ages working through that output, which you can see in any previous post, let’s actually try to solve part one.

main 1

// -- snip --
fn main() -> Result<(), Box<dyn Error>>{
    if let Ok(text) = read_challenge_input("./input.txt") {
        let mut santa: CoOrd = CoOrd::new();
        let mut yr_1_map: HashMap<CoOrd, i32> = HashMap::new();
        text.chars().for_each(|c| {
            *yr_1_map.entry(santa).or_insert(0) += 1;
            if let Ok(dir) = Direction::read_direction(c) {
                santa.mutate_coord(dir);
            }
        });
        println!("year 1: {}", yr_1_map.len());
    }

    Ok(())
}
// -- snip --

As always, there’s a lot wrong above, in particular I think I’m working against myself here but let’s just talk it out.

On line 2 above we start the new version of main() keeping the same Result contract as before (more on that in a minute). Then on line 3 we have switched to an if let statement where we ask

Does read_challenge_input give us Ok(text)?

if it does we move inwards (in scope) and onwards (in triumph) using text as a variable containing our input chars.

On line 4 we declare a new CoOrd called santa, followed on line 5 by a new HashMap which we’ve type annotated to hold a CoOrd as the key and an i32 as the count.

Thus endeth the set up.

The actual attempt at a solution happens next. I’m using a closure within the for_each consuming-adaptor on an Iterator over the chars in text to:

  1. check santa's current location, adding a new entry to the HashMap or incrementing the counter for that location by 1.
  2. check if Direction::read_direction(c) gives us a viable Direction rather than a DirectionError and if it does,
  3. mutate santa (meaning change x or y as our mutate_coord dictates)

All this should give us each co-ordinate santa hits paired with a count of how many times he’s been there.

Once the closure is finished, assuming we got no errors, we then print the length of the HashMap, which is just a count of all the k-v pairs.

Before I run it I want to comment a little on it, and maybe lay out a prediction or two.

In the first instance, I’m not propogating any errors upwards anymore, and this means that my hilarious Result<(), Box<dyn Error>> implementation isn’t as useful anymore. The if let version is unpacking the Result<T, E> our hand rolled functions give us and so at this moment I’m not sure what will actually happen if I force an error… I should do that once I have a working solve.

Secondly, I’m leaning a lot on closures so far in my AOC journey. This is because they seem like something relatively novel to rust (to this MidWit anyway) and something worth getting reps in with. I know that I could just do this with standard loops but… well, in addition to needing practice with rusty code… I…

I don’t like loops that much… There… I said it… I’m sorry but that’s just how I feel.

It’s not that I hate them or anything, I just… I don’t like using them that much. Even when working in python, if I can, I’ll reach for a comprehension.

And yes, sometimes I’ll actively try to write the stupidest comprehension I can think of, and have indeed spent ages trying to get a comprehension to work when a relatively simple nested loop would have worked.

And been easier to read…

And easier to debug…

And faster to write…

Don’t look at me like that, Derrick. I’m not alone in this

I’m sure I’m wrong here, but closures feel like comprehensions and for now they’re fun to use. I get that there are probably times where loops are the right tool for the job. But this ain’t a job, this is me trying to learn and have “fun”5

Ok, let us submit our humble offering to Friend Computer and see if it is deemed acceptable.

:!cargo run
   Compiling day_3 v0.1.0 (/home/aoc/day_3)
error[E0599]: the method `entry` exists for struct `HashMap<CoOrd, i32>`, but its trait bounds were not satisfied
  --> day_3/src/main.rs:13:23
   |
13 |             *yr_1_map.entry(santa).or_insert(0) += 1;
   |                       ^^^^^ method cannot be called on `HashMap<CoOrd, i32>` due to unsatisfied trait bounds
   |
  ::: day_3/src/coord.rs:5:1
   |
 5 | pub struct CoOrd {
   | ---------------- doesn't satisfy `CoOrd: Eq` or `CoOrd: Hash`
   |
   = note: the following trait bounds were not satisfied:
           `CoOrd: Eq`
           `CoOrd: Hash`
help: consider annotating `CoOrd` with `#[derive(Eq, Hash, PartialEq)]`
  --> day_3/src/coord.rs:5:1
   |
 5 + #[derive(Eq, Hash, PartialEq)]
 6 | pub struct CoOrd {
   |

For more information about this error, try `rustc --explain E0599`.
error: could not compile `day_3` (bin "day_3") due to 1 previous error

shell returned 101

I’m starting to feel like House M.D. except instead of Lupus it’s Traits… At least I’ve actually seen a new thing here: Hash is a Trait and looking at this output it seems that it needs to be there for a type to be able to act as the key in a HashMap.

Let’s go ahead, follow Friend Computer's sage advice, and re-run the code.

:!cargo run
   Compiling day_3 v0.1.0 (/home/aoc/day_3)
error[E0507]: cannot move out of `santa`, a captured variable in an `FnMut` closure
  --> day_3/src/main.rs:13:29
   |
10 |         let mut santa: CoOrd = CoOrd::new();
   |             ---------  ----- move occurs because `santa` has type `CoOrd`, which does not implement the `Copy` trait
   |             |
   |             captured outer variable
11 |         let mut yr_1_map: HashMap<CoOrd, i32> = HashMap::new();
12 |         text.chars().for_each(|c| {
   |                               --- captured by this `FnMut` closure
13 |             *yr_1_map.entry(santa).or_insert(0) += 1;
   |                             ^^^^^ `santa` is moved here
   |
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but `FnOnce` closures may consu
me them only once
  --> /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/iter/traits/iterator.rs:838:11
note: if `CoOrd` implemented `Clone`, you could clone the value
  --> day_3/src/coord.rs:5:1
   |
 5 | pub struct CoOrd {
   | ^^^^^^^^^^^^^^^^ consider implementing `Clone` for this type
   |
  ::: day_3/src/main.rs:13:29
   |
13 |             *yr_1_map.entry(santa).or_insert(0) += 1;
   |                             ----- you could clone this value

For more information about this error, try `rustc --explain E0507`.
error: could not compile `day_3` (bin "day_3") due to 1 previous errors

There it is!!! I was actually thinkin’ that I might get away without one but we found it pals!! A RUST ROOKIE MISTAKE!!

I would love to shake my fist in the air, with some real gravitas, and shout Traits! Like a villain scorned. But this one isn’t just about the lack of Copy/Clone on my CoOrd, and indeed Friend Computer's guidance here is both wise and generous, telling us how to fix the thing. However, understanding the why, would help me avoid the error in future (he said with unearned optimism) and reinforce some learning from my last post6.

sigh

Closures, capturing, and consumption.

I’ve made no secret of the fact that I’m not a computer scientist, engineer, or developer; in fact I’ve shouted it from the rooftops and prefaced way too many sections of this blog with the disclaimer. Part of this is because it’s true and useful context, and part of it is ego-defence. I’m basically saying “I’m sorry this thing that is obvious to you isn’t obvious to me, but I’m not steeped in it like you are, please be nice”.

In fairness the vast majority of feedback that I’ve gotten from the community has been very generous, and curious, and welcoming, even when my mental models are genuinely alien. But, the truth is that learning Rust makes me feel stupid a lot (which is no bad thing) and so I hedge and disclaim to make me feel better.

But, guys, this one makes me feel really stupid… Like really really, double plus dumb.

From my reading of The Book, closures are ‘anonymous’ functions like python’s lambdas. They’re designed to let us bundle together operations in an impermanent way. We can specify a parameter or two, do a thing or two, and then throw them away when not needed anymore. They can “capture” values from the surrounding environment and let us do something with them in an ordered way, but not in a generalised way which can then be applied to other things in our program.

The reality is that they’re not functions, or rather they are not a subset of the “function” idea7. They are actually more like a nameless Struct with implicit Trait implementations, which govern how they interact with the values they capture from the surrounding environment. The operations we perform inside the closure determine which Traits are implemented, how values get captured and what we can do with them.

The truth is that I don’t have enough reps in with this yet and every time I try to unpack it fully I seem to get tied up in knots. But let’s see if this time around, with this specific error, I can get a little further into it.

There are three Fn Traits related to closures, enumerated in The Book thusly:

  • FnOnce applies to closures that can be called once. All closures implement at least this trait because all closures can be called. A closure that moves captured values out of its body will only implement FnOnce and none of the other Fn traits because it can only be called once.
  • FnMut applies to closures that don’t move captured values out of their body but might mutate the captured values. These closures can be called more than once.
  • Fn applies to closures that don’t move captured values out of their body and don’t mutate captured values, as well as closures that capture nothing from their environment. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently.

Essentially each of these is a promise the closure makes about what it won’t do with any captured values. They are referred to as additive in this chapter of the book, and they are, but in a way that felt (and still feels) counter-intuitive. The Fn Trait basically is a promise not to do anything other than read the value it has captured, so it is immutably borrowing the value. It can be called many times in the same way that we can have any number of immutable borrows of a value. And, this is important, it can be called concurrently by multiple threads.

FnMut means that the value can be mutably borrowed and so it can be changed through out the process. It can be called multiple times sequentially but not concurrently because we can only have one mutable borrow at a time.

FnOnce just means that the closure can be called at least once, but closures that “move” a captured value out will only implement this trait and not the others. Basically, rather than taking &self or &mut self, this Trait implies that something in the closure is moving the actual value.

Which is exactly what’s happening in my closure.

// -- snip --
        text.chars().for_each(|c| {
            *yr_1_map.entry(santa).or_insert(0) += 1;
            if let Ok(dir) = Direction::read_direction(c) {
                santa.mutate_coord(dir);
            }
// -- snip --

On line 3 in the snippet above santa is passed into the .entry() method and the value inside santa is being “moved out” of santa and passed into the HashMap as a new key if it doesn’t already exist. santa is passed by value into entry and so it’s not available to be used again, it is “consumed” by the .entry() call.

I’m going to stop here a second and articulate one of those things that’s probably really obvious to everyone but me.

When we use let to assign a value to a variable we’re basically creating a bucket that contains the value. If we borrow (Fn) we’re just looking in the bucket at whats there. If we mutably borrow (FnMut) we’re reaching into the bucket and modifying what’s in there in someway (or at least we’re allowed to). In the case up above though, what we’re doing is taking the CoOrd that is in the santa bucket and putting it somewhere else. So essentially santa has no value to do anything with.

The bucket is not the water within it. The body is not the soul that animates it. The CoOrd value isn’t the variable it is bound to.

See? Obvious.

for_each wants a closure that can run multiple times, once per char, making changes to the captured value as it does (FnMut) but the current closure can’t because santa is empty after the first call, there’s nothing there8. So the FnOnce promise is the only one the closure can make. The other two can’t be satisfied and so the program won’t compile. My implementation wants FnMut, but the current code doesn’t allow it.

If we derive the Copy Trait on our CoOrd the entry() call will get it’s own santa to play with which we don’t need to explicitly make in any way. If I derive Clone I can explicitly call the .clone() method, which does a very similar thing, but it’s plain to see in the text. Being a midwit I like to avoid things being hidden, cause they’re too easy to miss.

All of that to say that the solution is to derive Clone on my Struct and then add .clone() inside the entry call.

// coord.rs 
// -- snip --
#[derive(Debug, Clone,  Eq, Hash, PartialEq)]
pub struct CoOrd {
    x: i32,
    y: i32
}
// -- snip --

// main.rs 
fn main() -> Result<(), Box<dyn Error>>{
    if let Ok(text) = read_challenge_input("./input.txt") {
        let mut santa: CoOrd = CoOrd::new();
        let mut yr_1_map: HashMap<CoOrd, i32> = HashMap::new();
        text.chars().for_each(|c| {
            *yr_1_map.entry(santa.clone()).or_insert(0) += 1;
            if let Ok(dir) = Direction::read_direction(c) {
                santa.mutate_coord(dir);
            }
        });
        println!("year 1: {}", yr_1_map.len());
    }

    Ok(())
}

which gives me the output

:!cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `/home/aoc/day_3`
year 1: 2592

Boom!

Nearly 400 lines into this puzzle and we have a solution to the puzzle.

Phew.

I have some thoughts on this, but I’m pretty sure they need time to settle so I’ll move onto solving part 2

Part 2

As always if you want to know the full puzzle please go and check out the Advent of Code, but in short, for part 2 Santa now has a robot assistant that is taking every second step (I imagine it was trained on Santa’s previous performance and will soon automate away the whole job, but that’s progress, right?). We still need to count up how many CoOrds we land on total, but now we have two different paths we’re tracking.

I’m 90% sure that the closure approach up above will work, but I just need to think about how I might apply it selectively, alternating between our two actors.

There’s a couple of ways I might do this:

  1. Use a boolean to track who should be moving, and switch it after each move.
  2. Enumerate over the Chars checking if the index is odd or even.
  3. Use some kind of offset as I run through the chars.

Looking at the options above, 3 is the least interesting to me. Offsets aren’t Rust specific and at a cursory glance it seems unnecessarily fiddly. Options 1 and 2 both require some kind of conditional logic which lets me get more reps in with the match statement so I’m leaning towards them. In my python work I often find that I use enumerate(df.columns)9 and so that is a comfortable idea for me, and better yet, it gives me the option to apply guards in my match statement, which is new to me.

Which puts me on option 2 applying my sloppy and inconsistent approach to decision making. Push myself a little, but not too much. It’s kept me firmly in the middle for 40 years now, and I’ll be damned if I’m going to change at my time of life!!

// main.rs 
// -- snip -- 
        // year 2
        santa = CoOrd::new();
        let mut robo_santa = santa.clone();
        let mut santa_map: HashMap<CoOrd, i32> = HashMap::new();
        let mut robo_map: HashMap<CoOrd, i32> = HashMap::new();
        *santa_map.entry(santa.clone()).or_insert(0) += 1;
        *robo_map.entry(robo_santa.clone()).or_insert(0) += 1;
        text.chars().enumerate().for_each(|(i, c)| {
            match i {
            i if i % 2 == 0  => {
                *santa_map.entry(santa.clone()).or_insert(0) += 1;
                if let Ok(dir) = Direction::read_direction(c) {
                    santa.mutate_coord(dir);
                }
            }, 
            i if i % 2 == 1 =>  {
                *robo_map.entry(robo_santa.clone()).or_insert(0) += 1;
                if let Ok(dir) = Direction::read_direction(c) {
                    robo_santa.mutate_coord(dir);
                }
            },
            _ => unreachable!(),
        }}
        );
        println!("year 2: {}", santa_map.len() + robo_map.len());
    }
// -- snip --

I’ve just focused on new code in the snippet above, but I have left the part one solve alone.

Ok, so given as this is a new year we need to reset santa and give him his own santa_map, which you can see up on lines 4 and 6. Then on line 5 I use .clone() (praise be to Friend Computer) to create robo_santa at the same starting position as santa, giving him his own HashMap on line 7. Finally, on lines 8 and 9, santa and robo_santa take their first step in their respective maps, both starting at 0,0 which lets us finish the set up.

Line 10 is where the real fun begins. The main change here is that we’ve inserted the .enumerate() adaptor between text.chars() and .for_each() to give us both the index and the actual Char (|(i, c)|) as we step along the chain of input. Again, I know that I could use a for loop here, but I wanted to see how I can use comparison, or guards, in a match statement and the wonderful Rust by example has this lovely demo here, which absolutely saved me from another Rookie Mistake.

Starting on line 11 you can see that I’m just checking to see if i % 2 produces 0 or 1, the only two options in the real world. However, apparently Friend Computer (though wise and benevolent) doesn’t care for our real world constraints, and so we have to put in a _ => unreachable!() catch all even though it’s impossible. I have to say I really like that there is a clear way of stating that this thing is unreachable but it has to be there as part of a limitation with the language.

For the record I did run the code with out the catch-all and I got E0004.

Then once the closure is finished I have two maps of the CoOrds visited on each path and I can print the combined length to get the total.

Let’s cross fingers and toes and see what we get back.

:!cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `/home/aoc/day_3`
year 1: 2592
year 2: 2499

Welp, I’m kinda “Schrodinger’s right” here, which, as always, is better than just plain wrong. I’m gonna take all the wins I can get on this journey.

We got no errors: Boom, however my answer is wrong…

And sadly, this ain’t a Rust Mistake, it’s a tiredness mistake, which again is pretty obvious, when you look at it.

Both santa and robo_santa can land on a point while they are making their way around, and so by just adding up the length of both maps I’m double counting some of the points; I need to combine both maps to get the right .len().

There’s probably some neat trick to do this, but right now I think that the .entry() api and a cheeky for loop will get me home faster. And the Lord knows that this post doesn’t need to be a lot longer.

// -- snip --
        // year 2
        santa = CoOrd::new();
        let mut robo_santa = santa.clone();
        let mut santa_map: HashMap<CoOrd, i32> = HashMap::new();
        let mut robo_map: HashMap<CoOrd, i32> = HashMap::new();
        *santa_map.entry(santa.clone()).or_insert(0) += 1;
        *robo_map.entry(robo_santa.clone()).or_insert(0) += 1;
        text.chars().enumerate().for_each(|(i, c)| {
            match i {
            i if i % 2 == 0  => {
                *santa_map.entry(santa.clone()).or_insert(0) += 1;
                if let Ok(dir) = Direction::read_direction(c) {
                    santa.mutate_coord(dir);
                }
            }, 
            i if i % 2 == 1 =>  {
                *robo_map.entry(robo_santa.clone()).or_insert(0) += 1;
                if let Ok(dir) = Direction::read_direction(c) {
                    robo_santa.mutate_coord(dir);
                }
            },
            _ => unreachable!(),
        }}
        );
        for (coord, _count) in robo_map {
            santa_map.entry(coord).or_insert(0) ;
        }
        println!("year 2: {}", santa_map.len());
    }
// -- snip --

The change here is pretty self explanatory, I’m iterating over robo_map and if a key isn’t already in santa_map we’re inserting it with a value of 0. We’re not incrementing anything, increasing the number of times a point was hit, but we could modify the code to do so, it’s just not necessary for the puzzle solve.

Let’s run it and check the output.

// -- snip --
   Compiling day_3 v0.1.0 (/home/aoc/day_3)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.29s
     Running `/home/aoc/day_3`
year 1: 2592
year 2: 2360

And we have reached an unequivocated, bona fide BOOM!

The code runs, the answer is correct, and I believe most of it is decent enough Rust, while acknowledging that the architecture probably isn’t great. I could certainly re-arrange things to put less code in the file and still get the same result.

Full solve

So here’s my whole main.rs file for anyone who wants to read over it. As I said before, some of the suggestions from previous readers have been very very helpful. Doesn’t make me less nervous to post, but it has been a nice side effect.

mod coord; 

use std::collections::HashMap;
use std::error::Error;
use aoc_common::read_challenge_input;
use crate::coord::{CoOrd, Direction};

fn main() -> Result<(), Box<dyn Error>>{
    if let Ok(text) = read_challenge_input("./input.txt") {
        // year 1
        let mut santa: CoOrd = CoOrd::new();
        let mut yr_1_map: HashMap<CoOrd, i32> = HashMap::new();
        text.chars().for_each(|c| {
            *yr_1_map.entry(santa.clone()).or_insert(0) += 1;
            if let Ok(dir) = Direction::read_direction(c) {
                santa.mutate_coord(dir);
            }
        });
        println!("year 1: {}", yr_1_map.len());

        // year 2
        santa = CoOrd::new();
        let mut robo_santa = santa.clone();
        let mut santa_map: HashMap<CoOrd, i32> = HashMap::new();
        let mut robo_map: HashMap<CoOrd, i32> = HashMap::new();
        *santa_map.entry(santa.clone()).or_insert(0) += 1;
        *robo_map.entry(robo_santa.clone()).or_insert(0) += 1;
        text.chars().enumerate().for_each(|(i, c)| {
            match i {
            i if i % 2 == 0  => {
                *santa_map.entry(santa.clone()).or_insert(0) += 1;
                if let Ok(dir) = Direction::read_direction(c) {
                    santa.mutate_coord(dir);
                }
            }, 
            i if i % 2 == 1 =>  {
                *robo_map.entry(robo_santa.clone()).or_insert(0) += 1;
                if let Ok(dir) = Direction::read_direction(c) {
                    robo_santa.mutate_coord(dir);
                }
            },
            _ => unreachable!(),
        }}
        );
        for (coord, _count) in robo_map {
            santa_map.entry(coord).or_insert(0) ;
        }
        println!("year 2: {}", santa_map.len());
    }
    Ok(())
}

I’m absolutely not going to commit to this, but, I might (might) come back in future and see if I can refine my implementations. I did that with some of them when I worked through the AOC using python and it was fun to compare my two approaches.

It’s long past time I wrapped this beast up though.

Reflections

Working my way through this one was mostly fun, and it feels like this one pushed me more than some others, not so much on the syntax or Rust but on things happening under the hood.

As I said above, the whole Fn Trait thing makes me feel really quite stupid and I had to circle it for ages to make any sense of it, but based on some of the github issues on The Book’s repo it hasn’t been an easy thing to make clear. It feels like a place where ownership, move semantics the reality of functions as empty Structs and closures as Structs with Traits and state conspire against the novice. Each thing on it’s own is complex enough (and there’s nothing wrong with complexity) but stacking them in this way makes it hard to explain without complication.

The Fn Traits are not additional powers you get, they are more like limits that nest inside each other. FnOnce means that something can be called at least once not only once. FnMut means we are allowed to run multiple times (satisfying FnOnce) and we are prepared for the captured value to be borrowed mutably and altered as part of the process. Fn just borrows immutably and so it does less than FnMut allows, it stays within that bound, and thus fulfills all three promises. The way this is sticking in my head is the difference between a “promise” and a “contract” (which I understand is tenuous and pedantic and not actually linguistically correct, but go with me on this journey).

A contract is a commitment to do something, we state that we’ll only give our object such and such kind of input and it will only give us such and such output. It is active. A promise (again, just go with it) is an agreement not to go beyond a certain limit. I promise I’ll do this and no more. I had been thinking of the names of the Fn Traits, especially FnMut similarly to a function signature or a type annotation: “I must do this”. But that’s wrong, it’s really “I promise not to do more than this”.

This really drives home, though, that one really just needs a lot of upfront knowledge to write Rust. One needs to know, for example, that .entry() consumes, or drops, or eats up, or “moves out”, the value that’s passed to it, determining that the closure can only implement FnOnce. I know that now because I’ve worked through that problem, which is great, and the point of this whole exercise, but honestly, it’s a little overwhelming to know that there’s just so much to keep in mind when trying to do something ostensibly simple.

Knowing you’re a midwit is to know that you know nothing. Y’know?

I get that needing a lot of upfront info is a feature of Rust rather than a bug, knowledge is what empowers people to make decisions and I’m all about it. S’tough though.

Something I am really delighted about is that this really drove home to me the difference between the variable and the value. It’s something that I knew (there’s that word again) as a result of the last post, but I didn’t understand it until I saw it in this context.

I was thinking of it as two layers, we have the value and a reference to that value, Self and &self respectively. But in reality, we have three layers, the value, the reference, and the bucket we assign that value to. Well, not in reality, this is all virtual, but that’s the best I can do with it for now.

Using let to assign a value to a variable just creates a slot on a shelf that we put the value in, the value can be “moved out” of that slot so that there’s nothing in it anymore. Again, I touched this before, and I could articulate it, but I didn’t really see it until this post.

I wonder how many more times I’ll realise that I haven’t seen it as I continue.

Anywho, that’s enough of my guff for this post. I’ve been travelling with family over the summer so this one might feel a little more disjointed as I could only dip into it here and there.

I hope you enjoyed the ride, thanks again for your time.

If you learned anything from it yourself I’m really glad, but if I were you I’d take every line of code I have with a pinch of salt.

Until next time, God’s Speed,

The MidWit:wqa

References (for nerds)

Amabile, Teresa M., and Steven J. Kramer. 2011. The Progress Principle: Using Small Wins to Ignite Joy, Engagement, and Creativity at Work. Harvard Business Review Press.
Bandura, Albert, and Dale H. Schunk. 1981. “Cultivating Competence, Self-Efficacy, and Intrinsic Interest Through Proximal Self-Motivation.” Journal of Personality and Social Psychology 41 (3): 586–98. https://doi.org/10.1037/0022-3514.41.3.586.
Cepeda, Nicholas J., Harold Pashler, Edward Vul, John T. Wixted, and Doug Rohrer. 2006. “Distributed Practice in Verbal Recall Tasks: A Review and Quantitative Synthesis.” Psychological Bulletin 132 (3): 354–80. https://doi.org/10.1037/0033-2909.132.3.354.
Fredrickson, Barbara L. 2001. “The Role of Positive Emotions in Positive Psychology: The Broaden-and-Build Theory of Positive Emotions.” American Psychologist 56 (3): 218–26. https://doi.org/10.1037/0003-066X.56.3.218.

Footnotes

  1. I’ll be honest, I agonised a little about how to lay out this file. I was certain I would be judged harshly on the layout of the code, and that there’s some perfect way to do it that all Rust users already know but I just hadn’t seen yet. Then I remembered that this is the internet and no matter which way I do it, someone won’t like it, but so far people have been very generous when pointing out things I can do better so it just makes sense to be consistent in my decisions. I decided to keep all the object definitions together and then put all the implementation blocks below them. If there’s a better way to do it someone’ll tell me. This learning out loud thing is kind of pointless otherwise.↩︎

  2. Is that the plural? Enum feels Latin so maybe Eni? Or Ennui, depending on how you’re feeling?↩︎

  3. Using a big word lightly there.↩︎

  4. But if you do want to learn about it then this book is well worth a read, especially in a world of LLM’s. Just a thought.↩︎

  5. Y’know, the kind of fun where you feel stupid all the time, and get a bad headache… like drinking booze.↩︎

  6. Hard not to feel a little betrayed right now.↩︎

  7. It’s actually the other way around, but if I tried to explain that I’d have an anyur… anyeur… a stroke.↩︎

  8. Jesus that’s bleak↩︎

  9. Don’t @ me about iterating over columns in a dataframe until you’ve had to clean up psychology research data from 10 different sources none of which you designed and which were created by a team of undergrads and stressed out PhD students.↩︎