Advent of Code 2020: Thoughts

(With this, I think I just have managed to avoid going a full year with nothing posted, phew!)

2020 is nearly over, and with it, so is this year’s Advent of Code. While I’ve participated in the past, I’ve always petered out around day 13 or 14. This year, on the other hand, thanks to a combination of the pandemic, transitioning from college work to a full-time job, and some novice programmer friends taking an interest, I actually managed to complete all1 the challenges.

I don’t really have enough interesting thoughts to do a problem-by-problem running commentary, but I figured it might be useful to note down some thoughts I had looking back.

Note that I’m going to reference later parts of problems without warning, so if you care about spoilers, you should stop reading.

Language choice

I’ve been meaning, for a few years now, to learn the J programming language. Array languages are famous for producing solutions that are dazzlingly short and completely inscrutable, as they provide very concise ways of expressing powerful dataset manipulations, such as folds, maps, transpositions, and more exotic things besides. Complementary to this is a culture of tacit programming, the J variant of which makes Haskell’s point-free style feel like programming with goto.

Coming into the less trivial parts of this year, I had pipe dreams of forcing myself to do the entire rest of the calendar in J. Perhaps unsurprisingly, I didn’t make it very far, but I definitely learned a lot. Being able to easily apply multiple operations to the same value(s) and then chain the results (ala the “monadic/dyadic fork” from the link above) is brain-bending, but even after only a few days I’ve come to miss it in Haskell and OCaml – it’s honestly easier than convincing Haskell that your carefully-constructed arrow combinator typechecks and that you didn’t forget a flip or uncurry somewhere. I’d like to do a detailed breakdown of some of the J solutions I’m proud of another day, but who knows when I’ll have the energy to blog again.

Aside from general language difficulty vs my personal frustration, I actually think that this year was an especially unlucky one for aspiring array programmers. Days 7, 8, 14 and especially days 18 and 21 had nontrivially-formatted inputs or required some other kind of complex parsing logic. I’m sure I could have worked something out for some of them given enough time, but the downside of having a full-time job is that, well, it takes time that would otherwise be spent thinking about parsing with dyads, or whatever.

After giving up on J, I defaulted back to good old Haskell, as usual (aside from a particular day’s challenge done in Rust, because finding all the right places to put strictness annotations just isn’t my idea of a good time). I managed to avoid touching mtl (or any other effect library, for that matter) entirely, which I’m grateful for. This not being Real Software, I appreciated being able to get by with iterate, containers, and the odd parser combinator.

Version Control

In a lot of ways, I think little challenges like this aren’t great fits for version control. There’s never any need to go back to already-written code, no maintenance, no need to rewrite old challenges with new functionality, and most importantly, you’re the only one working on the project. At best, you might have a few iterations of a file over the course of a single challenge, but at this level your editor’s undo functionality will do just as well.

Of course, the advent of Github means that you should probably use some git if you want to share your stuff, even if it isn’t strictly the intended use case. This year, I ran parallel pijul and git repositories to track my AoC work, mostly to learn the former while still being able to share on Github.

In general, I wouldn’t recommend it. pijul is alpha software and it shows, with lots of little usability gripes (pijul record always pulls up a text editor unless you commit all changes in the repository, which makes writing scripts that rely on it quite annoying). There’s also the annoyance of needing to commit and push everything twice. Honestly, the factors in the first paragraph that make AoC a bad fit for version control saved me here – as I never needed to revert, rebase or merge, we had a nice linear history for both VCS’s to follow, and they never stepped on each other’s toes by editing the shared worktree.

That said, the experience wasn’t without merit. I am currently working on a proper git-pijul bridge to solve the “do everything twice” problem, and have more than a few ideas about how to embed pijul’s simpler operations into git’s more complex commit graph. Stay tuned.

Difficulty

One thing I prefer about AoC to, say, Project Euler, is that, in most cases, the sizes of the inputs are pretty reasonable. “Do the same thing again, but more” is not what I came here for (there’s a reason I flamed out of the competitive programming team after high school…), and it can often give unfair disadvantages to certain programming paradigms (see: Haskell’s immutability + pervasive laziness causing memory overflows that you would not get implementing the same algorithm in, say, Java). That isn’t to say that optimization can’t be fun, but I already need to do enough of it in my day job that I’d prefer to spend my free time on something else.

This year, thankfully, only had two of them, days 15 and 23. In the first case, even with the proper approach, my program instance got OOMkilled before getting remotely close to the asked-for 30,000,000th value, so I’m not sure what I missed. Maybe I should have printed some small cases and looked for a pattern, but eh. The solution for day 23’s gigantic problem was actually interesting, because there was an actual algorithmic insight to be had (and even then, I had to rewrite it to use a mutable array in Rust instead of a strict AVL-backed dictionary in Haskell).

There were also a weird number of “encode this cellular automata” problems, with at least three actual Game of Life implementations I can think of offhand. I’m familiar enough with the canonical efficient implementation that I didn’t run into any issues, but it got a bit tedious at times.

All in all, I think this year was generally of a more-even difficulty than last year’s (in which I lost interest at day 13). I do kind of miss Intcode (figuring out that Haskell’s laziness lets you get away with some absurd recursive stuff was a rush), but the mix of graph problems, mathematical insights (thanks, Tribonacci!), brute force (search for these sea monsters, why don’t you?) and mind-bending business rules (Recursive Combat took a few tries).

Other scattered thoughts

Will I do this again?

I hope so! I had a lot of fun, even if I wasn’t able to achieve my goal of becoming a J guru. Only time will tell if I’ll have the self-control to solve all of these, even when we’re allowed to go outside (fingers crossed…). The early days are always trivial enough that I’d like to try something new, but I’m running out of ideas. I could always give J another shot, or do all point-free in Haskell, or do it with Vim keystrokes.

Is it worth trying to rank?

I don’t think so. I was a competitive programmer in high school, but now that I’m a Real Adult (yeah, right) I just don’t have the patience to stay up till midnight and grind the quick ones. Yeah, I think I probably could make it consistently with some practice and luck, but I don’t see the point. I’ve scoreboarded exactly once (in 2019) already – I’ve proven to myself that I’m capable of it.

  1. Depending on when you read this, I may not have actually completed day 25 yet…