Advent of Code 2025


If you don’t know Advent of Code, it’s an annual puzzle hunt advent calendar put together by Eric Wastl where you help Santa’s elves prepare for Christmas by solving a series of increasingly contrived programming challenges. It’s a ton of fun and a great way to get hands-on experience with a programming language you’ve never tried before or are just beginning to learn. Last year I set out to solve all the challenges in Rust, and in the process I made tremendous strides in my understanding of the language. This year, I decided to tackle all the problems in Haskell.

Why Haskell?

If you aren’t familiar with Haskell, Wikipedia describes it as a “general-purpose, statically typed, purely functional programming language with type inference and lazy evaluation.” Being purely functional, the programming paradigm is very different from imperative languages like C, Rust, and JavaScript. Everything in Haskell is completely immutable, and all computations need to be expressed as pure functions with no side effects.

This means something as simple as a for loop, which is classically done by holding onto a loop variable, incrementing it after each iteration, and leaving the loop once it reaches a certain value, needs to be done in an unfamiliar way. Thankfully, many imperative languages have started including concepts borrowed from functional programming, such as map, filter, and reduce functions. My familiarity with these from non-functional languages helped me tremendously in getting started with Haskell. Also instrumental to my learning was Learn You a Haskell, which explains much of the language in very approachable terms.

My Approach and AI Usage

Ultimately, I was successful in completing all 12 problems from Advent of Code 2025. The learning curve was fairly gentle, and I was never stuck on a problem for too long because of issues with the language itself.

I should note up front that although AI assistance is generally frowned upon in Advent of Code, I did make limited use of AI this year. Since I started on the problems late (the calendar had already concluded by the time I began), I felt this was acceptable given the following constraints:

  1. Code review only - I only ever asked the AI to verify that the code I wrote was idiomatic Haskell. I wanted to come up with all the solutions myself and let the AI critique me the way a teacher would grade an assignment.
  2. No direct problem-solving - When I needed help getting code to compile, I never asked the AI for direct assistance with an AoC problem. I only ever presented it with very limited tasks devoid of any context that would allow it to help me solve the problem.
  3. Documentation clarification - Occasionally, I would ask the AI to help me understand Haskell documentation. Hoogle is a great resource, but at times it reads less like programming documentation and more like a mathematical research paper. In these cases, I found it useful to have the AI generate a minimal, contrived example of how a particular function was meant to be used, which I would then apply to my own code on my own.

With all that said, here’s how my Haskell learning journey went across all 12 days of Advent of Code 2025! I won’t describe the problems here, so if you want to see the prompts for context, please visit the 2025 event page and follow along.

Day 1: Getting Started

Day 1 was relatively easy. The problem was easily solvable using modular arithmetic, so I spent most of my time making sure I could get my environment set up right and write a basic program in Haskell.

Day 2: Repeating Digits

The problem on day 2 was quite interesting mathematically. The task was to identify which numbers in a list were made by repeating a sequence of digits. I realized that if a number was made of repeating digits (like 123123123 or 15551555), then it must be a multiple of an “eleven-like” number (111 or 1001001). I could also figure out exactly which eleven-like number it must be a multiple of based on the length and number of the repeating sections. From there it was as simple as testing if the number was divisible by that factor. This was a really cool way to start exploring the mathematical expressivity of Haskell.

Day 3: Dynamic Programming

Day 3 was simple enough, and the first time I got to apply dynamic programming techniques in Haskell. No major hurdles on this day either, but I was starting to feel somewhat confident in the language.

Day 4: The First Real Challenge

Day 4 was the first big challenge. I had to deal with a 2D array of data, and Haskell’s immutability made this problem harder than I expected. I learned about the Array data type, which I used extensively in later days, and I also wrote my first recursive function in Haskell.

The ability to pattern match in a function definition makes recursion in Haskell very concise. I particularly enjoyed being able to state the base case up front as if it were another function, instead of including it as a conditional at the beginning of the function. For some reason it feels much cleaner.

Day 5: Collapsing Ranges

A bit of a step down in difficulty from day 4. The only part of day 5 that took any work was figuring out how to collapse a list of potentially overlapping ranges into a list of non-overlapping ranges. This wasn’t hard though, and again I really enjoyed writing a recursive function in Haskell, this time for implementing a binary search.

Day 6: Function Application

Day 6 was again very simple. One feature of Haskell that made this day particularly easy was the $ function, which represents function application. I was able to create two lists and use zipWith ($) to apply the function from the first list to the argument in the second list. While this may be possible in another language, it would likely feel out of place and magical, while in Haskell it feels natural and elegant.

Day 7: Embracing Immutability

This day presented the first big conceptual challenge of this year’s event. Typically I would solve a problem like this by creating an initial state, repeatedly modifying it until the end, and then reading out the result. Since Haskell is immutable though, I had to think harder about how each step of the computation could be expressed more precisely as a function of the state before. It took me a little while, but eventually I worked it out, and it was very rewarding to have the solution expressed in only pure functions.

Day 8: Graph Algorithms

Day 8 was another instance of just applying an algorithm I learned in school for identifying connected components in a graph. I was impressed that the code I wrote all fit on one screen. In fact, the longest source file I had at this point was only 64 lines long, and that’s without using convoluted tricks to cram things into one line that really ought to be spread across five or six. I continue to be impressed by Haskell’s expressivity.

Day 9: Discovering Monads

This day was difficult because although I could immediately identify the algorithm I wanted to use, I had a lot of trouble implementing it correctly. I eventually learned about the Control.Monad.State monad, which allows you to keep track of a stateful computation.

I won’t get into the details of what exactly a monad is (a monad is a monoid in the category of endofunctors, what’s the problem?) — just know that they’re extremely useful, and in fact necessary, when trying to do any computation in Haskell that includes something like a side effect. That includes keeping track of state (with the State monad), using IO (with the IO monad), or memoizing a function (using the Memo monad).

I was eventually able to implement the point-in-polygon algorithm in a way that was satisfactory to me. Only then did I realize that not only was my implementation too slow, but it was entirely unnecessary. Rather than testing every point on the interior of a polygon to see if it was inside another polygon, I simply had to check if any of the edges of the two polygons intersected. Not only did the new solution massively improve the runtime, it also allowed me to reduce the entire day’s worth of code to just 48 lines.

Day 10: The Marathon

This was the hard one. Day 10 took me several days to complete, and the journey was long and winding. I felt very clever in part 1 for reducing the problem to a few bitwise operations, but part 2 was significantly more challenging.

My first thought was that it was essentially a system of linear Diophantine equations, which I remembered learning about in my Number Theory course. But I had more unknowns than equations and wasn’t sure how to find the particular solution with the smallest L1 norm. After more research, I realized it wasn’t a system of Diophantine equations at all, but rather a simple linear programming problem that I’d learned to solve in high school.

I wasn’t content to simply grab an off-the-shelf package for solving linear programming problems in Haskell, so I set out to write my own. I hadn’t watched a Patrick JMT video since high school, but I found myself back on his channel learning about how to solve my exact problem using the Big M method. Once I’d implemented that algorithm, I learned that the ideal solutions for the problem weren’t integer-valued, so I had to also implement a Branch and Bound algorithm to trim the non-integer solutions and eventually end up with the one I was looking for.

Day 11: The Power of Memoization

Day 11 was a return to simplicity. I was able to simply apply a dynamic programming algorithm I knew offhand to the problem and have a solution very quickly. The only thing I had to do was learn how to use another monad, this time Control.Monad.Memo. Once I figured out how it worked, it felt like magic. With nearly zero effort, I was able to memoize my function and take it from being so memory-hungry that it froze my entire computer to completing in less than a second.

Day 12: The Finale

The final day of Advent of Code this year. I was surprised, as historically they’ve ended on the 24th. I was a bit disappointed with the problem on this day because I immediately recognized it as a variant of rectangle packing, which is NP-hard. That meant the intended solution was likely based on heuristics that were only knowable after looking at the particular input given to you.

I was able to solve the problem, but my solution works only for real puzzle inputs, not for the example input given in the problem statement. While I’m reasonably confident it was the intended solution, not having a program that’s capable of solving the problem for all possible inputs is somehow unsatisfying to me. Either way, I was able to solve the problem and complete the last day of the year!

Final Thoughts

Overall, I very much enjoyed solving these problems in Haskell. As usual, the Advent of Code problems were just challenging enough to require intentional concentration and clean programming, but not so difficult that they were unreasonable for an experienced programmer to complete.

As for Haskell, I really enjoyed learning the language. I found it immensely satisfying to write code in such an expressive manner, and I found the harsh limitation of immutability to be simultaneously freeing. Since mutation was off the table, I was able to think only in terms of strict mathematical calculations, which led to some really neat code.

I definitely don’t feel like I have as good an understanding of Haskell at the end of this event as I did of Rust at the end of last year’s event, but I definitely intend to find a side project to use Haskell for in the future so I can get more comfortable with the parts of the language I have yet to touch.