Overview

I’ve always enjoyed participating in the Advent of Code, even if I never end up finishing the whole thing. The event comes at a bad time for me, where finals exams, co-op interviews, and holidays intersect in such a way that it’s impossible to commit to hours of extracurricular coding each week. However, I like to use AoC as an excuse to play around with a language that I wouldn’t otherwise put my hands on. This year, I chose to experiment with Nim. I hadn’t heard of Nim until a friend mentioned it to me shortly prior to AoC. The tl;dr on Nim is that it’s statically typed and compiles to JavaScript and the C family. I have described it as being the illegitimate child of Python and TypeScript, which are two of my favorite languages to use. Here are some of the most enjoyable + quirky aspects of Nim that I uncovered as I worked through Advent of Code in 2021:

(It’s unfortunate that my Hugo theme doesn’t support syntax highlighting for Nim… Just imagine it in your mind 🙂!)

Day 1

I had no clue what I was doing with Nim when I started Day 1. I was just using it as I’d use Python, and praying that nothing would blow up.


proc findIncreases(nums: seq[int]): int =
    for i in 1 .. nums.len - 1:
        if nums[i] > nums[i - 1]:
            result += 1

I really liked the idea of this result variable, which is a shorthand for declaring and returning some kind of output variable. Nim automagically returns the value of result once your proc finishes executing. Coolio!

Day 2

Day 2 was a more challenging problem, and required me to use some of the more complex features of Nim. As a statically typed language enjoyer, I started to dabble in Nim typing. I really liked their synatx for initializing typed variables. I also really liked tuples:


proc getFinalPosition(commands: seq[tuple[direction: string, x:int]]): tuple[horizontal: int, depth: int] =
    var 
        hz: int
        depth: int

I think that I used tuples appropriate for this method, but throughout AoC, I may have overused them. They’re a nifty data structure, but I tended to use tuples as a way of overcomplicating methods. This would lead to a devastating refactor if Part 2 of a problem conflicted with my initial division of labor in my procs.

Day 4

This is when things started to get bad, you guys. Day 4 is around the time when I began to feel frustrated with Nim – or rather, my inability to conform my problem-solving to its limitations. I brute-forced the thing, but still wrote a few enjoyable one-liners.

For instance: strNums.keepItIf(it != "") # remove blank lines Although I would become frustrated with the absence of certain utils in sequtils, I really like keepItIf.

By Day 4, I also had developed a stubborn attachment for explicitly typing as much as I could. This works out pretty well until you start dealing with 3D arrays, i.e. an array of bingo boards. (To be fair, you would have this same issue with Java… I think the concise nature of seq just makes it harder to look at than ArrayList<ArrayList<ArrayList>>) This is not enjoyable to write or maintain:


proc boardLoop(commands: seq[int], boards: seq[seq[seq[int]]]): int =
    var currBoards: seq[seq[seq[int]]] = boards
    for s in commands:
        var checkedBoards: seq[seq[seq[int]]] = checkBoards(s, currBoards)

If it weren’t for finals week, I would have been motivated to figure out this puzzle:


proc transpose(s: seq[seq[int]]): seq[seq[int]] = # don't care to figure out how to do this with map()
  result = newSeq[seq[int]](s[0].len)
  for i in 0 .. s[0].high:
    result[i] = newSeq[int](s.len)
    for j in 0 .. s.high:
      result[i][j] = s[j][i]

This is what I wrote to transpose a bingo board, or flip the board such that rows became columns and columns became rows. This problem isn’t even Nim-related, moreso related to my lack of experience in functional programming. If I were solving this in Python, I could’ve used something like numpy or even zip() to transpose the 2D-array. With Nim, you gotta bake that from scratch!

Day 5

By Day 5, I had discovered object types in Nim, which was cool. But I was also starting to run into minor trouble:


proc getInput(fileName: string): seq[tuple[startPos: Position, endPos: Position]] =
    var strPos: seq[string] = readFile(fileName).strip().splitLines()
    for str in strPos:
        var rawPos: seq[string] = str.split("->")
        var posTuple: tuple[startPos: Position, endPos: Position]
        var posSeq: seq[Position] # this is stupid.
        for pos in rawPos:
            var cleanedPos: seq[int] = pos.strip().split(",").map(parseInt)
            posSeq.add(Position(x: cleanedPos[0], y: cleanedPos[1]))
        posTuple = (posSeq[0], posSeq[1])
        result.add(posTuple)

In this method, I’m trying to read a file of Position inputs formatted like: 0,9 -> 5,9. I want to parse each set of digit to a Position, and then store both in a tuple. I was not able to find a way to populate a tuple one element at a time, which was understandable but frustrating for my use-case. I would have liked to just declare a posTuple, and then .add() up to 2 elements to it, just as I would do with a seq. Maybe I could have found a workaround by doing something like posTuple = (posOne, null), but then that would require me to keep track of whether I was on posOne or posTwo.

Another issue that I ran into during this problem was the matter of tuple iteration. In Python, you can use a simple for ... in to accomplish this task. I couldn’t get the naive solution working for tuple iteration (for x in t.fields:), and I was intimidated by some of the complex StackOverflow solutions that I found. I didn’t want to spend an hour figuring out how to iterate over a data structure, especially when a lot of the answers that I found online boiled down to “just use a different data structure”. So I did!

Day 6

Here’s something positive about Nim: CountTables are dope! Helpful data structures like these make me wish that Nim were an accepted language on Leetcode 😆. Day 6 was a breath of fresh air after the last 2 tricky problems, and I liked CountTable. This was also the first time that I timed my solution in Nim, which I understood to be breathtakingly fast. Using the ‘counter implementation’ for this particular problem, part 1 and 2 of my code executed in 0.002 seconds.


var newCounts: CountTable[int]
for k in countdown(8, 0):
    var fishHere: int = currFish[k]
    if k == 0: # flip to 6, create new fish
        newCounts.inc(6, fishHere)
        newCounts.inc(8, fishHere)
    else: # standard case, i think
        newCounts.inc(k - 1, fishHere)

Day 9

This was the day that broke me. I had been able to cope with 8 days of yearning for non-seq data structures, but Day 9 broke me. I recognized that this was a flood fill problem, and initially I felt like Nim and I were up for the task. Things got out of hand quickly. Remember what I said before about misusing tuples? Yeah:


proc findBasinSize(heightmap: seq[seq[int]], low: tuple[x: int, y: int], visited: Table[tuple[x: int, y: int], bool], size: int): 
        tuple[basinSize: int, visited: Table[tuple[x: int, y: int], bool]] =

I did not enjoy that very much. Still, for Part 1 I was able to get a working, hack-y solution for finding adjacent elements in a 2D-array and finding the low points of a 2D-array as I traversed it. The killing blow came in Part 2, where it became apparent that I would need to use some kind of BFS/DFS approach to find the largest basins in my puzzle input. The problem? Nim doesn’t have anything like a stack or queue, so I’d have to implement that data structure from scratch. (Not impossible, but probably not simple either.) Furthermore, I felt punished by any attempts at implementing a recursive solution. I encountered a bunch of weird nuance, and decided that I would just forfeit on Part 2 and get back in the game tomorrow. As I told my friends, nim recursion making me angy + i don't care to implement stack in a new language from scratch at this time. So true!

Day 10

But I was wrong 🙃. I immediately recognized that Day 10 of AoC was a twist on a paren-matching problem, which I have no problem solving in the typical language. I recognized that, if I had just went about creating my own Stack implementation in Nim, I probably would’ve saved myself a lot of trouble throughout AoC. I was immersed in Sunk Cost Fallacy at this point as I realized that my peers using Python would be spending 1/2 the time on AoC that I spent each day. It was tempting to just switch languages to continue working through AoC problems, but what’s the fun in that! So I just gave up.

Retrospective

Despite my angst, this was one of the most fun Advent of Codes that I’ve ever participated in! I got much further than I expected, and I learned a good deal about a cool new language. My key Nim takeaways were:

  • It’s fun to write in a concise, statically-typed language. Probably my favorite language archetype out there.
  • Nim is blazing fast. This is something that you definitely don’t get when you work in Java or Python for these types of problems. It was satisfying to compile and run new code in the blink of an eye, especially when I was going through trial-and-error of various solutions.
  • The Nim docs are excellent. Yes, there’s some room for improvement. I think there are certain portions that could be described or organized better. But I lived in this documentation while I worked through AoC, and I didn’t hate it that badly.
  • The Nim community is incredible. It’s made up of some super supportive people who are dedicated to empowering and promoting a cool, cutting-edge language. Their StackOverflow answers and forum threads were invaluable to me as I was learning the ropes of Nim.

To check out my GitHub repo for Advent of Code 2021, click the link here!