Another Advent of Code is over, so it's time to review how it went for me.

The first thing is to thank Eric Wastl and his team for another excellent event and set of challenges. I always look forward to these events, both because of the challenge of the puzzles themselves, and because it's the one time I year I can get to program in Haskell. I think that over time, I'm becoming more familiar with Haskell and the tools provided by the language's ecosystem, so my solutions are making better use of existing features and less reinventing wheels.

(You may like to compare this review with the review of 2021 and review of 2020.)

The puzzles

My day job is a computing senior lecturer (vaguely equivalent to "associate professor" for our American colleagues), which means I have high standards and a critical eye when it comes to describing tasks for others.

These puzzles are great!

The tasks descriptions are very clear, with excellent examples. For every puzzle this year, I tested my code against the supplied example. There were only one or two places where my code worked correctly on the example input and not on the real input.

My only one niggle was with day 20, where the example didn't have a case where the offset was larger than the size of the list being rotated.

Programming concepts

The Advent of Code site claims that

You don't need a computer science background to participate - just a little programming knowledge and some problem solving skills will get you pretty far.

I think that's true. Most of the puzzles involved simple programming concepts, such as some basic data structures and some fundamental algorithms. Graph search was a prominent motif this year, along with decisions about how to represent a seemingly-unrelated task to a graph of states that can be searched.

Another concept was the evaluation of a structure, such as collapsing a set of operations into a summary value, as we saw with the shouting monkeys in day 21. What I don't think we've seen so far is where the same structure is evaluated in different ways, akin to applying a different algebra to the same structure and interpreting it differently. (I don't count day 2's rock-paper-scissors, as that has the same text parsed in different ways.)

Generally, most of the problems required a bit of thought and then a little bit of programming. There were few edge cases to deal with, and no input validation. That made the solutions generally short.

A few concepts that were out of the ordinary were:

Haskell is a terse language. The solutions are generally short, mostly around 100 lines of code (including imports and the like) The median is 97 lines of code. Note that programs 12, 16, 19, and 24 reused a standard implementation of A* that takes about 100 lines.

Program name Lines of code

In addition, the declarative and functional basis of Haskell meant that I could express many problems very clearly. I could concentrate on what the problem was, rather than getting bogged down in the detail of how to find a solution. For example, for Day 4 I just wrote down some facts about interval relations and that was enough to express the solution. Laziness and infinite streams helped express several problems concisely, such as the cycle-finding in day 17 that made heavy use of infinite simulations which I then filtered to find the interesting elements.

Haskell's typeclass feature came in handy a couple of times. It allowed me to reuse almost all the code for the two parts of day 16, and a custom definition of the Ord typeclass made day 13 almost trivially easy.

Performance

The Advent of Code site states that

every problem has a solution that completes in at most 15 seconds on ten-year-old hardware.

I failed here, with four of my solutions taking over a minute to execute, and one taking nearly 20 minutes! Expect some posts in the near future on optimising these programs and reducing the execution times. I was disappointed that many solutions took as along as they did, but I didn't do any particular optimisation on any of them. Instead, I chose (what was to me) the obvious approach to each problem and was happy if it worked.

Memory use was another surprise. All bar two of the programs ran in small amounts of memory, around a megabyte or so. The two that ran longer were the beast what was day 19, and a surprising amount of memory usage for day 15. That is likely to be the allocation of about four million points to explore for beacons, and is a case where some laziness in the program could have led to a better solution.

Another perspective on memory use is the number of memory allocations made. Many of these are short-lived and reclaimed by the garbage collector. As you can see, in most cases the memory churn follows the memory use.

If you want the numbers for time and memory use, here's the table.

Program   Elapsed time (s)   Max memory (kb)

Language features

I took a look at the packages and modules I used. (A module is something imported into a Haskell program; a package is a collection of modules downloaded and installed together. containers is a package, but Data.Set is a module.)

Package Days of use
containers 16
attoparsec 14
lens 14
text 14
linear 9
mtl 7
pqueue 4
split 4
array 2
multiset 2
data-clist 1
deepseq 1
parallel 1
rosezipper 1

This shows I used containers on most days for things like Sets and Maps, along with attoparsec and text for parsing the input data files. lens got a lot of use, as I now tend to use it for most fiddling with data structures. linear was used to represent points and positions. mtl (monad transformers) was used the few times I used monads in the code (four of them were in the graph-search programs).

I did a similar analysis at the module level.

Module Number of uses
AoC 25
Data.List 16
Data.Text 14
Data.Attoparsec.Text 14
Data.Text.IO 14
Control.Lens 13
Data.Set 11
Data.Maybe 10
Control.Applicative 10
Linear 9
Data.Map.Strict 5
Data.Ix 5
Data.Sequence 4
Data.List.Split 4
Data.Char 4
Data.IntMap.Strict 3
Data.Array.IArray 2
Data.MultiSet 2
Data.Ord 2
Data.PQueue.Prio.Max 2
Data.PQueue.Prio.Min 2
Data.Tree 1
Data.Tree.Zipper 1
GHC.Generics 1
Control.DeepSeq 1
Data.Monoid 1
Data.IntMap 1
Data.Foldable 1
Data.CircularList 1
Control.Parallel.Strategies 1
Prelude 1

This shows that List, Set, and Maybe are the most-used data structures. I was a bit surprised that Map only occurred eight times (five times as Map, thrice as IntMap). Prelude occured once, to hide a standard import.

As with the previous couple of years, none of the packages and modules are "exotic": there's no use of rings or groups, no constraint solvers, no uses of number theory. I used monoids once because I felt like, more than the problem deserved it.