Advent of Code: Postmortem

Looks like it’s that time of the year again. I’m on a train with nothing to do, and I’ve been procrastinating this for months.

Last year, I attempted a challenge for Advent of Code. If you want a detailed explanation, check out the blog post I wrote back then. tl;dr: I had a set of 5 programming languages for the 25 tasks. I had to use each of them 5 times.

Even though I failed, it didn’t turn out that bad. I finished 14.5 days. 1-14 are fully implemented, and for 16, I did the first half. Then, I stopped.

I’m not here to make excuses, but humor me for a paragraph before we get to the actual content of this.

I really disliked day 15. Not because it was too hard, but because it would have been boring to implement while also being time-consuming due to the needless complexity. I just didn’t feel like doing that, and that apparently set a dangerous precedent that would kill the challenge for me.

So there’s that. Now for the interesting part:

The part you’re actually here for

I tried out languages. Languages that I had never really used before. Here are my experiences:

C

Days finished: 4

Before the challenge, I knew absolutely nothing about C. I had never allocated memory, incremented a pointer, or used section 3 of man. That’s why I wanted to get this out of the way as quickly as possible.

C is interesting. It lets you do all these dirty things, and doing them was a kind of guilty pleasure for me. Manually setting nullbytes or array pointers around. Nothing about that is special, but other languages just don’t let you. You know it’s bad, and that only makes it better.

Would I use C for other private projects? No. Definitely not. I just don’t see the point in $currentYear. But was it interesting? You bet.

Go

Days finished: 3

Allegedly, this language was made by some very smart people. They may have been smart, but they may have created the most boring programming language in existence. It feels bad to write, and it’s a terrible choice when dealing mostly with numbers (as is the case with most AoC puzzles). It’s probably great for business logic, and I can say from personal experience that it also works quite well for web development and things like discord bots. But to me, not having map/reduce/filter/etc. just makes a language really unenjoyable and verbose.

Writing Go for AoC sometimes felt like writing a boring version of C (TL note: one that won’t segfault and memory leak your ass off if you don’t pay attention).

People say it’s more readable and all that, and that’s certainly great for huge projects, but for something like this… I wouldn’t ever pick Go voluntarily.

And also not for anything else, to be perfectly honest. I mean… just look at this. (Yes, I wrote this. Scroll down and use the comment section to tell me that I’m just too dumb to understand Go.)

package main

import "fmt"

func main() {
    // This declares a mutable variable.
    var regularString = "asdf"
    // This also declares a mutable variable.
    // I have no idea why there are two ways of doing this.
    unicodeString := "aä漢字"
    for char := range(unicodeString) {
        // You might expect that this prints the characters individually,
        fmt.Println(char)
        /*
         * Instead, it compiles and prints (0, 1, 3, 6) -- the index of the first byte of each character.
         * Very readable and very intuitive. Definitely what the user would want here.
         */
    }
    for _, char := range(unicodeString) {
         /*
          * Having learned from our past mistakes, we assign the index to _ to discard it.
          * Surely this time.
          */
         fmt.Println(char)
         /*
          * Or not because this prints (97, 228, 28450, 23383) -- the unicode indices of the characters.
          * Printing a rune (the type Go uses to represent individual characters,
          * e.g. during string iteration) actually prints its integer value.
          */
    }
    for _, char := range(unicodeString) {
        /*
         * This actually does what you’d expect.
         * It also handles unicode beautifully, instead of just iterating over the bytes.
         */
         fmt.Printf("%c\n", char)
    }
    /*
     * So go knows what a character is and how many of those are in a string when iterating.
     * Intuitively, this would also apply to the built-in len() function.
     * However...
     */
    fmt.Println(len(regularString)) // prints 4
    fmt.Println(len(unicodeString)) // prints 9
}

Oh, and there are no generics. Moving on.

Kotlin

Days finished: 3

Oh Kotlin. Kotlin is great. A few weeks after AoC, I actually started writing Kotlin at work, and it’s lovely. It fixes almost all of the issues people had with Java while maintaining perfect interoperability, and it’s also an amazing language just by itself.

I like the way Kotlin uses scopes and lambdas for everything, and the elvis operator makes dealing with nullability much easier. Simple quality of life improvements (like assignments from try/catch blocks), things let() and use(), proper built-in singletons, and probably more that I’m forgetting make this a very pleasant language. Would recommend.

In case you didn’t know: Kotlin even compiles to native binaries if you’re not using any Java libraries (although that can be hard because you sometimes just need the Java stdlib).

Python

Days finished: 2

I don’t think there’s much to say here. Python was my fallback for difficult days because I just feel very comfortable writing it. The standard library is the best thing since proper type inference, and it supports all the syntactic sugar that anyone could ask for. If a language is similar to Python, I’ll probably like it.

Yes, I’ve tried nim.

Rust

Days finished: 2

Rust is… I don’t even know. But first things first: I like Rust.
I like its way of making bad code hard to write.
I like the crate ecosystem.
I like list operations and convenience functions like sort_by_key.
I like immutability by default.
I like generics (suck it, Go).

Not that I didn’t have all kinds of issues with it, but Rust made me feel like those issues were my fault, rather than the fault of the language. I also wouldn’t say I feel even remotely comfortable with the borrow checker -- it sometimes (or more often than I’d like to admit) still felt like educated trial and error. I’m sure this gets better as you grow more accustomed to the language, and so far I haven’t encountered anything that would be a deal breaker for me.

Rust might even become my go-to language for performance-sensitive tasks at some point. It definitely has all of the necessary tools. Unlike some languages that leave you no room for optimization with intrinsics or similar magic. (Why do I keep going back to insulting Go? Why does Go keep giving me reasons for doing so?)

The borrow checker will likely always be a source of issues, but I think that is something that is worth getting used to. The ideas behind it are good enough to justify the hassle.

See you next year. Maybe.

I underestimated just how little time and motivation I’d have left after an 8-hour workday that already mostly consists of programming.

It was fun, though, and I’ll probably at least try something similar next year.

Let’s see what stupid challenge I come up with this time.

Did anyone actually expect me to succeed?

Leave a comment