Advent of Code 2021

Synopsis and reflections on solving the Advent of Code 2021 challenges through various programming languages.

Advent of Code 2021

If you're new to Advent of Code (AOC), you can read more about it for some background info.

This was my second year completing the full set of challenges and obtaining all 50 stars. These challenges were released in December last year, but I only completed the first few challenges then and completed the rest this summer. The stress and study of the Spring term made me put off AOC until I was utterly and completely bored at work with nothing to do.

Some people like to play AOC competitively. It has a global leaderboard as well as a private leaderboard (so you can compare yourself against others in a group). For me, I rather take my time about it; figure out problems slowly without the pressure of a race.

This year's AOC story leads us diving deep underwater trying to find a key or something to kickstart Santa's sleigh. The author really put some effort into creating the wackiest of scenarios. There were also a plethora of sea animals among the challenges: from giant squids, to cute little dumbo octopi, to crabs.

I tried using Haskell and Rust. Initially, I planned to solve every challenge in both Haskell and Rust, but I realise this is quite annoying. And after gaining the star, I'm slightly less motivated to solve it in a second language. So I opted to use Haskell and/or Rust. (One challenge was solved using Python.) Perhaps some day I'll solve the challenges in both languages.

Both Haskell and Rust are modern programming languages in design. Haskell is heavy on functional programming, whereas Rust is multi-paradigm. Both are general-purpose PLs, so both have a wealth of libraries and frameworks.

I continued using Haskell Stack for compilation. It's somewhat inefficient. The way I set up my project, I listed each day as a separate executable. This way I could do something like stack run d01, and it would run my code for the challenges on the first day. Previously, due to some issue with its dependencies, Stack will compile all executables and modules within the project... and. that. is super. annoying. 🤮 Since then, I've tweaked the module and project setup for faster compile times. Anyhow, I continue using Stack as its dependency resolution is good, and I could extend my code with unit testing or benchmarking if I'm up for it in the future.

Several problems lent themselves well to functional programming, especially the parsing ones (Day 10, Day 16). I tried learning the ST monad (which is how Haskell magically jumps between the immutable and mutable worlds) and tried applying it for Day 25. The functions (writeArray, newSTRef, writeSTRef) were verbose, and didn't have the same inherent beauty of array[i] = 0 in imperative languages. But from an academic perspective, it was interesting to read about ST and how Haskell bridged the gap between pure functions and state mutations.

In last year's AOC, I tried solving everything with Haskell, but was suuuper annoyed with simulation challenges. These were challenges asking us to do stuff like: "oh, please simulate this 1,000x1,000 grid for 1,000,000 steps with Conway's game of life". With a purely functional PL like Haskell, this isn't easy. Taking a functional approach is extremely costly, as you would be building entirely new grids each step. Haskell does have imperative programming and mutability (via monad magic)... but it's not Haskell's strong suit, and coding-wise, you need to jump through so many mental hoops to get it working properly. And it wasn't exactly beginner-friendly.

Patrick tries to scare Squidward with x equals x plus 1. Stop it Patrick, you'll scare him!

So this year, I decided to use Rust for most of the challenges where mutability is essential to the algorithm. Rust seems to be emerging in industry, has solid tooling, and a growing community; I think it's more than enough reason to try learning it through AOC.

Rust's package manager, cargo, was fast. The documentation, Rust Book, was extremely helpful and cleanly edited. I ran into some issues where adding a println! caused the compiled binary to not run (OS error, could not run executable?). But this only happened on my work desktop. Probably a security thing.

Also, all the hype I see about Rust's error messages is completely justified. They really did put a lot of effort into explaining the nuances of the Rust compiler. And they have pretty colours as well. 😱 On a couple of occasions, the error messages don't help at all (e.g. adding dyn to an impl trait in a function type?); but usually there is some blog post explaining and providing a proper solution to the problem.

Overall, this year's AOC was a fun experience and the EUREKA moments were definitely worth it. It was nice having different programming languages available to use, as some PLs had more flexible approaches to some problems (e.g. solving Day 14 using Haskell's parser combinators), some had built-in features/design that made it easier to solve challenges (e.g. mutability in Day 25 with Rust), and some had more familiar libraries (e.g. z3 in Python for Day 24). If anything, this strengthens my belief that diversifying across PLs is a productive effort.

My solutions for this year have been uploaded online. But if you're interested in programming and problem solving, you should definitely give AOC a try first before peeking at my solutions! :P

Share on