Solving AoC with Haskell
Aug 11, 2026 · haskell, advent-of-code, aoc

Recent years, among developers popularized a platform which holds once in each year. And every time we get a unique and crazy problems from its author. And in this my very first post, I wanted to show how I organized my solutions in a Haskell way. Let’s start.

Abstractions are the best friend

if you abstract properly

In a declarative programming, an approach I liked is abstracting everything as possible and just use them without overhead. In a Haskell we do this by using 2 methods:

  1. generic programming
  2. ad-hoc polymorphism

Based on these methods, I created my own specific AoC solver engine and instead of every-time creating new different project or collecting all my solutions into a single verbose file, I just create very own solution module for each problem and can just register this solution into my runner engine. Let’s see how this works.

First of all, I created a type class called Solution:

class Solution a where
  -- day is a method to get information about day info of given solution
  day :: (Year, Day)
  -- parse parses given list of strings and returns parsed variant of @a@
  parse :: a -> [String] -> a
  -- part1 and part2 will take parsed input and returns solution for Part 1.
  part1 :: a -> [String]
  part2 :: a -> [String]

And engine itself is just a Haskell record with run method:

data RunnerMode = All | Last | Single (Year, Day)

data Runner = Runner
      { mode :: RunnerMode
      , solutions :: [Solution]
      }

run :: Runner -> IO ()

As you see, the engine is very simple, you just create a solution and get instance of Solution and you are ready. Just run it and get answer.

But (every textbook, tutorial have their but and this post also) this code does not work for multiple solutions, because, Runner only can get a single instance of Solution at a time, not multiple. You only register a single instance as many times, but you can’t multiple instances. Haskell has very different and crazy world to solve our problems and viola: ExistentialQuantification.

Using this extension we will wrap our solution types into a single type with the same type-class instances. You can read more about in existential quantification. This is like wrapping a concrete type into any in Golang. Let’s create our wrapper type:

data AnySolution = forall s. (Solution s) => AnySolution s

This type can be read like this: for all types with instance Solution, we can create a AnySolution using a constructor. And this type holds actual Solution type class implementation. So, our Runner is changed like this:

data Runner = Runner
      { mode :: RunnerMode
      , solutions :: [AnySolution]
      }

Constructing Runner is very easy, in our main module, we just use like this:

let runner = Runner {
    mode = Last
  , solutions = [ AnySolution D1
                , AnySolution D2
                -- and so on ...
                ]
}

run runner

Now let’s see example instance of Solution:

data Example = Example | ParsedExample String

instance Solution Example where
  day _ = (2026, 1)

  parse Example xs = ParsedExample $ intercalate xs
  parse _ _        = undefined

  part1 (ParsedExample s) = ["unsolved"]
  part1 _                 = undefined

  part2 (ParsedExample s) = ["unsolved"]
  part2 _                 = undefined

With simple abstraction, we just removed code duplication and we are writing just our solutions and we get more focus to solve a problem faster. But we can do more optimizations and remove boilerplates of pattern matching. For now within the scope of this post, this is enough.

If you read the post till here, thank you for your patience. I tried my best to write and explain my thoughts. Full source code, you can see in my github repository here.


← All posts