🎁 🎅 🦌 🎄 ❄️
December means a new set of creative programming challenges from the makers of the popular Advent of Code. The holiday-themed puzzles are a fun way to practice new language skills or to sharpen old ones. They are language-agnostic and typically increase in difficulty each day.
Each year it’s easy to find plenty of people publishing their own solutions to the problems either on Reddit, on GitHub, or on their own blogs. I’m going to be one of those people this year. While my solutions are almost purely for my own edification, I want to share my own approach to the challenges and the standards I’ve set for myself this year.
All my solutions are on GitHub here: joeykilpatrick/advent-of-code-2022
I write TypeScript every day and there are a lot of possible styles of programming in the language. The approach used by a developer is typically pretty indicative of their programming background. Part of becoming a better programmer is understanding that there aren’t right or wrong styles, there are only tradeoffs. Different developers may prioritize writing concisely, or writing quickly, or writing simple code, or fast code, or efficient code, or provably-correct code, or maintainable code, or clever code, or elegant code.
My goal this year is to write multiple solutions from different perspectives.
Other popular programming challenges like Leetcode place a premium on efficiency, for one reason because it is easily measured. Interview prep is overwhelmingly focused on producing code that has low runtime cost or memory usage. And rightfully so, those are the kind of questions that get asked in interviews. But code isn’t weaker just because it is less efficient than other solutions. In the real world there are many use cases for code that runs slower but is more maintainable, or more readable, or simpler. In educational contexts, elegant or clever solutions might be the goal to help broaden minds or connect seemingly unrelated concepts.
While others may strive for runtime efficiency, I am not going to worry about it. I’m not going to go out of my way to limit the number of times I loop over a list or worry about things like memoization or tail recursion. You are welcome to do so in your own solutions.
The provided input for each problem is given as a
.txt file. Many people do file I/O and dedicate a large portion of their solution to parsing the format of the input text. I see the generic input format not as part of the challenge, but a necessary consequence of designing something language-agnostic.
When I get the input for each challenge, I reformat it with regular expressions to put in into a format that is easily digestible for each solution. The text is integrated directly into the code.
I don’t write each of these solutions every day, but here are the different ones I try to write.
This is typically the one I write first and is the easiest for me to write. It looks the most like what I would write when working with a team. The focus is on readability and maintainability.
Normally I define types specific to the problem, and then write functions that make use of those types. Common patterns are extensive use of the Array prototype methods like
Array.map, Object prototype methods like
Object.values, as well as lots of IIFE expressions. When TypeScript assertions need to be used, they have justifications in the comments.
I might pull in the
lodash utility library for common patterns like
This solution is the one that I would expect a beginner programmer to write. The focus for this one is to write simpler code that does not make use of features that would not be known to novices. There will be a high frequency of chained if-statements, indexed for-loops, and global state variables. Even so, the solution will not allow unsafe type assertions that would be common among beginners.
Calling this Object-Oriented Programming is laughable to most Java and C# developers, but this is as close to OOP as most TypeScript developers get. Classes are used whenever the challenge can be modeled well with an object-oriented approach. Not all challenges lend themselves easily to this solution. Sometimes TypeScript’s flexible interface and object structure is a more attractive option. The focus for this one is writing clear code that models the real-life objects that the problem describes.
This is always the solution I am the most proud of. It also happens to always be the most performant. I am a novice Haskell programmer, but I thoroughly enjoy writing in the language. Functional programming with Haskell is extremely powerful and elegant.
I feel like there is such an enormous learning curve for Haskell when coming from modern imperative languages. I have worked to learn the basics of the language multiple times but have stumbled trying to wade into intermediate level challenges. There are still some important core concepts that I have not fully grasped like the
State monad and the
Applicative typeclass. The focus for this one is to just get a solution that compiles and that produces the correct answer to the challenge. For me, anything beyond that is a stretch. This is where I hope to grow the most this year.
After writing the Haskell solution, I try to use the
fp-ts library to bring the same programming style to TypeScript. I have not used the library much in my day-to-day, but it gives developers the opportunity to write elegant functional code even if it doesn’t see the same performance as Haskell. The code is much more expressive than standard TypeScript and can be very elegant. The key benefit of the library seems to be the ability to gradually ease into the functional programming style, interoperating with existing TypeScript styles as needed. Even though the learning curve is steep, the library is much more approachable than a pure Haskell solution for many problems. I’m hoping to expand my functional programming knowledge here and hopefully take these lessons back to Haskell.