Advent of Code 2024 in Rust
tl;dr Did Advent of Code (AOC) 2024 in Rust and got 27 stars āļø
Last year I used Advent of Code (AOC) to try out Golang, this year I used AOC to learn rust.
The challenge where we had to find this christmas tree being made by a swarm of drones was pretty fun
The programming language is a detail
When you squint hard enough, the programming language you solve AOC with is an implementation detail.
I think itās nice if you can solve the problem āin your headā (on paper) and then you only need to write the implementation in any programming language, hence, a detail.
With python I did not feel the need to solve it on paper first. With rust I do.
I noticed that for the AOC challenges done in python you can just hack away at your liking because the language is very flexible and iterative.
For Rust and also Golang Iām noticing that I get a lot of value out of thinking of the whole flow first on paper, the data structures, and then programming it all in. I guess this is because the language is less flexible.
Rust is fast
Rust is faaast.
One thing I noticed immediately in the first couple of days is that rust is really fast. The first day my runtime of part 1 and 2 was 0.8ms. On the second day I got a runtime of part 1 and 2 of 13ms. Coming from Python this is like having super speed unlocked, which is an awesome feeling.
In Rust you ācan not use the same variable twiceā
Rustās ownership system is what makes it fast, but it also makes you think a bit about how memory is laid out. The learning curve is steep and Iām learning it now, or attempting to at least.
Hereās a small example, coming from someone that works with Python.
One rule of the ownership system is that every variable has one owner and exactly one owner. When this owner goes out of scope the variable is dropped.
A consequence of this is that you ācan not use the same variable twiceā. For example:
a=10 # a owns 10
b=foo(a) # b now owns 10, a no longer owns 10
Now you can not use a
anymore because ownership was given to b
.
This ownership system is what makes Rust fast, but also what makes it difficult to work with.
I know I did a terrible job explaining the ownership system, but this makes it somewhat understandable from a Pythonista perspective, I think.
I am using ChatGPT quite a bit and itās great (so far)
I output a lot of code with ChatGPT.
I was always a bit skeptical but I have to admit it is a big help.
Yes, I am writing a lot of shitty code. No it might not be the best code. But it is code that works and if I can output 10x the code at half the quality, I guess it is still a win?
For me, the challenge or learning curve is producing working rust code, not writing the best rust code.
For me, good now means working, whether I wrote it or ChatGPT wrote it.
When you think about it we write tests that fix down some behaviour and then we write code to make the tests pass. But there are many versions of the code that make the tests pass, so why not have ChatGPT write some?
If Iām really honest sometimes ChatGPT thinks of more edge cases than I do. Computers donāt make mistakes, humans do.
Honestly, ChatGPT is really great. Especially for fighting WITH the borrow checker. Inputting an Error gives me some explainer in normal people language that a noob like me can work with.
ChatGPT shouldnāt become a crutch that I should lean on too heavily.
But at the same time people use Google as a crutch as well. It works, for me, now.
I struggled with and learned how and when to use references
I struggled quite a lot in trying to understand when to use a borrow or a mutable borrow and in that struggle I think I gained some experience fighting the borrow checker.
Before, I would just change my code until it worked, but then I found some information on the internet
I learned that you want to follow the principle of LEAST permission. So the order goes something like this: first a borrow, then a mutable borrow, and then full variable ownership.
Or this one
Talking with another colleague he explained it as follows: Try to write functions that return new types themselves and then just pass in borrowed values, then you donāt need to think about ownership all that much.
In the beginning I leaned into ChatGPT really hard
I let ChatGPT write me a lot of rust code.
Iām not sure if this was a good move, but it produced working rust code. I realized it was/is a crutch that needs replacing at some point. But my whole point was not to write perfect rust code. The challenge for me was to write working rust code and that would solve the advent of code challenge.
Impressed by ChatGPT
I am quite impressed with the Rust code that ChatGPT writes.
It writes code that is literal to your spec and not elegant, but it is an AI trained on the collective minds of everyone that it was trained on. If you give good and atomic specifications, it does spit out working code, and thatās quite impressive to me.
I do have to admit that writing out the solution on paper helped a lot.
Reflections after day 4
Having small test cases for the normal use cases and edge cases helps a lot.
Having a test for reading in the data really helps.
Write out the solution on paper before and think about the functions you need.
In essence what you are making is a function mapping filename -> solution
You have to read in, parse, solve, and return, and thatās it.
ChatGPT is writing crazy good code.
Tests help A LOT with reproducibility of the solution.
TESTS HELP A LOT.
Can not be repeated often enough.
ChatGPT actually makes mistakes
I experienced that for some very simple parsing questions, ChatGPT made a lot of annoying off-by-one errors. I sort of expected it? Maybe? But it was still a bit annoying. An example:
convert this
............
........0. .. <== three dots ". .."
.....0 .. .. ..
.......0....
....0 .. .. .. .
......A.....
............
............
........A...
.........A..
............
............
to a vec<vec<char>>
Notice that the second line has 3 dots on the right.
However, the converted vector has four dots.
let grid: Vec<Vec<char>> = vec![
vec!['.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
vec!['.', '.', '.', '.', '.', '.', '.', '0', '.', '.', '.', '.'], four dots
vec!['.', '.', '.', '.', '.', '0', '.', '.', '.', '.', '.', '.'],
vec!['.', '.', '.', '.', '.', '.', '.', '0', '.', '.', '.', '.'],
vec!['.', '.', '.', '.', '0', '.', '.', '.', '.', '.', '.', '.'],
vec!['.', '.', '.', '.', '.', '.', 'A', '.', '.', '.', '.', '.'],
vec!['.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
vec!['.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
vec!['.', '.', '.', '.', '.', '.', '.', '.', 'A', '.', '.', '.'],
vec!['.', '.', '.', '.', '.', '.', '.', '.', '.', 'A', '.', '.'],
vec!['.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
vec!['.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
];
Trick: You can represent a grid of numbers with a hashmap
This is a technique that I saw somewhere in someoneās Rust solution and that I will be stealing for sure.
You can represent a grid like grid[row][col]
with a hashmap instead. This allows you to do out of bounds checking by simply checking if the key does or does not exist. This blew my mind.
Integer division fucked me up a couple of times
I learned painfully that integer division works like youād expect from integer division (
let num_a = p1 * d / denominator - p2 * b / denominator;
let num_b = p2 /denominator * a - p1 / denominator * c;
// let num_b = p2 /denominator * a - p1 / denominator * c;
Hashmap initialization
Learned how to do this initialization. There are probably nicer and better ways. But this is one of them I guess. Something I had to do quite often in AOC
Wrapping around grids can be annoying
There was one problem where I had to wrap around a grid which was a bit annoying. I solved this by taking the modulo in the end.
To fix this problem, we add numRows to i before taking the modulo numRows. This ensures that weāre always taking the modulo of a positive number. Now we can count the number of live cells among the eight neighbors of grid[row][col] as follows.
What I would have done differently
In hindsight I should have and would have liked to invest more in āthe basicsā
Basics like how do vectors work, what is .iter()
, how does .collect()
work etc.
But honestly without a really good reason to do these they are less FUN than just solving these little puzzles and grinding your way through.
Solving these programming puzzles, with no matter how shitty code, is fun and engaging.
Itās what keeps me learning.
So whatever you do, whatever crutch you need (I used ChatGPT), try to STAY ENGAGED!
Invest in debugging tools
One thing that I can suggest is to invest in debugging tools.
Like if you have a grid, ask ChatGPT to write a function to print it out.
Especially for the later puzzles, they are hard with many edge cases.
You will forget something and probably wonāt get it correct on the first try.
To hedge this, invest in your debugging toolkit where you can print out the state and have it match with the exercise at hand.
Mistakenly used binary XOR for to the power
I used 2^x
to try and to the power something, but this is the binary XOR operator.
let num = (A / 2^combo.get(&operand).expect(""));
This didnāt work as expected. Oops.
Thanks Reddit
I was stuck on some problem and then someone on Reddit gave this hint and I had exactly the same problem. I fixed the first division but there were 2 other instances I had to fix. Thanks Reddit.
Part 1 is a nice simple bytecode interpreter, but with quite a few opportunities for bugs. I coded it slowly and carefully, but forgot to actually do the division in the division opcode, and when I fixed the bug I forgot there were two other division opcodes to fix.
Comments