Files
2026-10-19 21:51:23 +02:00

25 KiB
Raw Permalink Blame History

Haskell Syllabus: Advent of Code 2026 Edition

Overview

Goal: Solve Advent of Code 2026 in Haskell. Focus on four core skills:

  1. Simple IO: reading and parsing string data from files
  2. String parsing: parsing and managing strings
  3. Data transformation: mapping, filtering, folding, grouping
  4. Terminal IO: printing and formatting data for the terminal

Plus the techniques AoC puzzles keep asking for: grids, search, simulation, memoization and performance.

Background: Proficient in Go, Rust and Python. Experienced with TypeScript, Java and C#. Prior functional programming in OCaml and Elixir.

Structure: 24 lessons in 6 units, about 60–90 minutes each, worked through at my own pace. Every 4th lesson is a practice session with real puzzles from past years. The next lesson is always the first unticked one.

Approach: Skip what I already know (immutability, recursion, ADTs, pattern matching, closures). Spend the time on what is different in Haskell: purity and IO, laziness, typeclasses, Text vs String, monadic parsing, and writing efficient code without mutation.

Unit Lessons Theme
1 1–4 Setup and foundations
2 5–8 Data transformation
3 9–12 IO and basic parsing
4 13–16 Parser combinators and terminal output
5 17–20 AoC techniques: grids, search, simulation
6 21–24 Performance, toolkit and mock puzzle days

How each session runs

  1. Warm-up (5–10 min): review items due in review.md.
  2. Concepts (20–30 min): the lesson's topics, explained against Rust/OCaml where it helps.
  3. Exercises (30–45 min): stubs with failing tests in src/Exercises/LessonNN.hs. I write the code; Claude gives hints, not solutions.
  4. Wrap-up (5 min): check criterion, update progress.md and review.md, tick the lesson, commit.

Practice sessions use past AoC puzzles. Solutions go in src/AoC/YYYY/DayNN.hs, puzzle inputs in inputs/YYYY/DayNN.txt. AoC asks people not to share their puzzle inputs, so inputs/ is in .gitignore.

Legend

  • [ ] not started · [x] done
  • Bridge: how a concept maps to a language I already know
  • Check: what I should be able to do without help at the end of the lesson

Unit 1: Setup and foundations

Lesson 1: Toolchain and AoC project setup

  • Lesson complete

Goals

  • Install GHC, cabal and HLS via GHCup; editor integration working (type-on-hover, errors inline).
  • Create the cabal project with a library, an executable and an hspec test suite (hspec-discover).
  • Learn the GHCi loop: :l, :r, :t, :i, :k, :doc, :set +s.
  • Install hlint and ormolu; know how to run them.
  • Set up the repo layout: src/Exercises/, src/AoC/, inputs/ (gitignored), test/.

Bridge: cabal ≈ cargo (cabal build, cabal run, cabal test, cabal repl). Hackage ≈ crates.io. Hoogle ≈ docs.rs, but you can search by type signature.

Exercises

  • Get cabal test green with one trivial test.
  • Add text, containers and megaparsec as dependencies and import each once to confirm they build.
  • Look up [a] -> Int, (a -> b) -> [a] -> [b] and String -> [String] on Hoogle. Note what you find.

Check: I can create, build, test and REPL into the project from scratch.

Lesson 2: Syntax, types and functions

  • Lesson complete

Goals

  • Function definitions, type signatures, currying, partial application, operator sections (+ 1), (/ 2).
  • where vs let, guards, case, pattern matching on tuples and lists.
  • Composition (.) and application ($); reading point-free code (and knowing when not to write it).
  • data, newtype, type; records and record update syntax; deriving Show, Eq, Ord.
  • Maybe and Either as the everyday error types.

Bridge

  • Maybe a ≈ Rust Option<T> / OCaml 'a option. Either e a ≈ Result<T, E> (note: error on the left).
  • data Dir = N | E | S | W ≈ OCaml variant / Rust enum.
  • newtype ≈ Rust tuple-struct newtype, zero runtime cost.
  • x & f (from Data.Function) ≈ Elixir/OCaml |>.

Exercises

  • Model a Dir type with turnLeft, turnRight, opposite, and move :: Dir -> (Int, Int) -> (Int, Int).
  • Write safeDiv :: Int -> Int -> Maybe Int and parseDir :: Char -> Either String Dir.
  • Rewrite three small functions in point-free style, then decide which version reads better.

Check: I can read a type signature and predict roughly what a function does before reading its body.

Lesson 3: Typeclasses, purity and laziness

  • Lesson complete

Goals

  • Typeclasses as interfaces: Show, Eq, Ord, Enum, Bounded, Num, Semigroup, Monoid; writing an instance by hand.
  • Constraints in signatures: (Ord a, Show a) => [a] -> String.
  • Purity: the "pure core, thin IO shell" design that every AoC solution will follow.
  • Laziness: infinite lists, take/iterate/cycle, and when laziness bites (space leaks from lazy folds).
  • Partial functions to avoid: head, tail, !!, read, fromJust. Safe alternatives.

Bridge

  • Typeclasses ≈ Rust traits (closest match), more than OCaml modules or Java interfaces.
  • Semigroup/Monoid ≈ impl Add plus an identity; <> combines, mempty is the empty value.
  • Laziness ≈ Rust iterators or Python generators, except it's the default everywhere.

Exercises

  • Derive Enum and Bounded for Dir and use them to rewrite turnRight without listing every case.
  • Write a Monoid instance for a Stats type (count, sum, min, max) and combine a list with mconcat.
  • Predict, then verify in GHCi: take 5 (filter even [1..]), length [undefined, undefined], foldl (+) 0 [1..10^7] vs foldl' (+) 0 [1..10^7] with :set +s.

Check: I can explain why foldl' is preferred over foldl, and design a program as pure functions plus a small main.

Lesson 4: Practice session 1

  • Lesson complete

Goals

  • Solve first real puzzles with what I know, using String, lines, words and read for now.
  • Establish the solution template: parse, part1, part2, main reading inputs/YYYY/DayNN.txt.
  • Test against the example input from the puzzle text before running on the real input.

Exercises

  • 2022 Day 1 (Calorie Counting), both parts
  • 2024 Day 1 (Historian Hysteria), both parts
  • Compare with the reference solutions afterwards; note idioms to adopt in review.md.

Check: I can go from puzzle text to a correct answer for an easy puzzle, with example-based tests.


Unit 2: Data transformation

Lesson 5: Lists and the core toolkit

  • Lesson complete

Goals

  • map, filter, foldr, foldl', zip, zipWith, zip3, concatMap, takeWhile, dropWhile, span, break, splitAt.
  • sum, product, maximum, minimum, and, or, any, all, elem, lookup.
  • List comprehensions vs map/filter chains; comprehensions for generating combinations and pairs.
  • Data.List: sort, sortOn, sortBy, group, groupBy, nub, partition, transpose, tails, inits, isPrefixOf, isInfixOf.
  • Data.Ord: comparing, Down for descending sorts.

Bridge

  • foldr/foldl' ≈ OCaml List.fold_right/List.fold_left, Rust .fold().
  • sortOn (Down . snd) ≈ Rust sort_by_key(|x| Reverse(x.1)).
  • [(x, y) | (x:ys) <- tails xs, y <- ys] ≈ all unique pairs, like Python itertools.combinations(xs, 2).

Exercises

  • Top-N: given [(String, Int)], return the 3 highest scores, ties broken by name.
  • Run-length encoding: "aaabcc" → [('a',3),('b',1),('c',2)] using group.
  • Sliding windows: windows 3 [1..6] → [[1,2,3],[2,3,4],[3,4,5],[4,5,6]].
  • All pairs of a list whose sum equals a target.
  • Transpose a grid of characters and find columns where all characters are the same.

Check: I can solve a typical "transform this list" task with library functions instead of hand-written recursion.

Lesson 6: Maps, sets and pipelines that can fail

  • Lesson complete

Goals

  • Data.Map.Strict (qualified as M): fromList, fromListWith, insertWith, adjust, alter, lookup, findWithDefault, toList, foldrWithKey, unionWith.
  • Data.Set: membership, union, intersection, difference; Data.IntMap for Int keys.
  • Counting and grouping idioms: frequency tables, group-by-key.
  • Functor and fmap/<$> on Maybe, Either, lists.
  • traverse/mapM to turn [Maybe a] into Maybe [a] (fail fast); mapMaybe and catMaybes to drop failures.

Bridge

  • M.fromListWith (+) ≈ Rust entry().or_insert(0) += 1 or Python Counter.
  • traverse parse xs ≈ Rust xs.iter().map(parse).collect::<Result<Vec<_>, _>>().
  • >>= on Maybe ≈ Rust .and_then(); <$> ≈ .map().

Exercises

  • Word frequency: count words in a string, case-insensitive, sorted by count desc.
  • Group (department, employee) pairs into Map String [String].
  • Validate a list of raw strings into Either String [Int], reporting the first bad value.
  • Find items present in list A but not B, and in both, using Set.

Check: I reach for Map/Set naturally, and I can chain Maybe/Either functions without nested case pyramids.

Lesson 7: State without mutation

  • Lesson complete

Goals

  • Threading state through a fold: foldl' with a tuple or record accumulator.
  • scanl for every intermediate state; iterate plus !!/takeWhile for repeated steps.
  • until and fixpoint iteration: repeat a step until nothing changes.
  • Cycle detection: store seen states in a Map, find the loop, skip ahead (a classic "run 1 billion steps" trick).
  • Recognizing the State monad at a reading level (get, put, modify), without depending on it yet.

Bridge

  • A fold with a record accumulator ≈ a Rust loop mutating a struct, written as a pure function step :: State -> Input -> State.
  • iterate step s0 ≈ an infinite Rust std::iter::successors.

Exercises

  • Simulate a robot following "RRUULD..." instructions; return its final position and all visited positions.
  • Find the first position visited twice.
  • Apply a state transformation 1,000,000,000 times using cycle detection.

Check: I can model any "simulate N steps" problem as a pure step function plus a fold or iterate.

Lesson 8: Practice session 2

  • Lesson complete

Exercises

  • 2022 Day 2 (Rock Paper Scissors), both parts: model the domain with ADTs and Enum.
  • 2024 Day 2 (Red-Nosed Reports), both parts: windows, all, removing one element.
  • 2021 Day 1 (Sonar Sweep), both parts: zipWith and sliding windows, ideally in two lines each.

Check: I solve easy puzzles with idiomatic list and map functions, without manual recursion.


Unit 3: IO and basic parsing

Lesson 9: IO, do-notation and files

  • Lesson complete

Goals

  • The IO type; do-notation; <- vs let inside do; pure/return.
  • Reading input: readFile, getContents, interact; writing: putStrLn, print.
  • Command-line arguments with getArgs; exiting with System.Exit.
  • mapM_, forM_, when, unless from Control.Monad.
  • An AoC runner: cabal run aoc -- 2024 1 picks the day's module and input file.

Bridge

  • do ≈ Rust ?-chained code or OCaml let*; <- "unwraps" a monadic value.
  • interact :: (String -> String) -> IO () ≈ a whole stdin-to-stdout filter in one line.

Exercises

  • wc clone: count lines, words and characters of a file given as an argument.
  • Build the AoC runner: takes year and day, reads the input, prints part 1 and part 2 with timings.
  • Print a usage message and exit with code 1 on bad arguments.

Check: I can run any solved puzzle with one command, and my solution modules contain no IO.

Lesson 10: Text and robust input

  • Lesson complete

Goals

  • Why String ([Char]) is slow, and when it's fine anyway (most AoC inputs are small).
  • Data.Text and Data.Text.IO (qualified as T/TIO); OverloadedStrings; T.pack/T.unpack.
  • Common operations: T.lines, T.words, T.splitOn, T.strip, T.breakOn, T.chunksOf, T.isPrefixOf, T.replace.
  • Handling blank-line-separated blocks (T.splitOn "\n\n"), trailing newlines and Windows line endings.
  • Handling IO errors: missing files, try with IOException, turning them into Either.

Bridge

  • String ≈ a linked list of chars; Text ≈ Rust String/&str.
  • try (readFile p) :: IO (Either IOException String) ≈ Rust fs::read_to_string(p) returning Result.

Exercises

  • Port the runner to Text input.
  • Parse blank-line-separated groups of numbers into [[Int]].
  • Read a key = value file into Map Text Text, ignoring blanks and # comments.
  • Print a friendly error instead of crashing when an input file is missing.

Check: I default to Text for input and never get tripped up by trailing newlines.

Lesson 11: Parsing without a library, and grids

  • Lesson complete

Goals

  • Splitting and reading: lines, words, T.splitOn, break/span on a delimiter.
  • readMaybe from Text.Read; Data.Text.Read (decimal, signed) for Text.
  • Data.Char: isDigit, isSpace, isAlpha, digitToInt.
  • Extracting all integers from a messy line (a very common AoC shortcut).
  • Parsing character grids into Map (Int, Int) Char with zip [0..].

Bridge

  • readMaybe s :: Maybe Int ≈ Rust s.parse::<i32>().ok().
  • Grid parsing with nested zip [0..] ≈ Python enumerate inside enumerate.

Exercises

  • ints :: Text -> [Int]: extract all (possibly negative) integers from "x=-3, y=12 to 7".
  • Parse lines like move 3 from 1 to 2 into a Command type.
  • Parse a character grid into Map (Int, Int) Char; find all coordinates of #.
  • Parse a CSV-like file into records, reporting the line number of every malformed row.

Check: I can turn any structured text into typed values with only base, text and containers.

Lesson 12: Practice session 3

  • Lesson complete

Exercises

  • 2022 Day 5 (Supply Stacks), both parts: a tricky two-section input with a drawn diagram.
  • 2023 Day 2 (Cube Conundrum), both parts: nested separators.
  • 2023 Day 1 (Trebuchet?!), both parts: string scanning with overlapping words.

Check: I can parse irregular inputs by hand, and I know when that's getting too painful.


Unit 4: Parser combinators and terminal output

Lesson 13: Parser combinators, part 1

  • Lesson complete

Goals

  • The idea: a parser is a value, and parsers combine into bigger parsers.
  • Applicative operators that make parsing readable: <$>, <*>, *>, <*, <|>.
  • megaparsec basics: Parsec Void Text, parse, errorBundlePretty, char, string, space, L.decimal, L.signed, sepBy, some, many.
  • Brief look at ReadP (in base, no dependency) for quick one-offs.

Bridge

  • ≈ Rust nom or chumsky, OCaml angstrom.
  • Point <$> int <* char ',' <*> int ≈ "parse an int, skip a comma, parse an int, build a Point".

Exercises

  • Parse "(3,4)" into Point 3 4.
  • Parse "Game 12: 3 red, 5 blue; 1 green" into a record with nested lists.
  • Make a parser fail on purpose and read the errorBundlePretty output.

Check: I can write a small combinator parser and explain what each operator does.

Lesson 14: Parser combinators, part 2

  • Lesson complete

Goals

  • Structuring a parser: lexeme and symbol helpers, whitespace handling, try and backtracking.
  • choice, optional, between, sepEndBy, manyTill, eof, anySingle.
  • Finding patterns inside noise: skipping junk until something parses.
  • Parsing keywords into sum types; why do-notation also works inside parsers.
  • A shared AoC.Parse module with my reusable helpers.

Bridge

  • try ≈ explicit backtracking; megaparsec doesn't backtrack by default, for good error messages.

Exercises

  • Parse arithmetic expressions with +, * and parentheses; evaluate them.
  • Extract every valid mul(a,b) from a corrupted string (the core of 2024 Day 3).
  • Parse a small INI file into Map Text (Map Text Text).

Check: I can parse a realistic, messy text format end to end with good error messages.

Lesson 15: Terminal output and debugging

  • Lesson complete

Goals

  • show vs custom rendering; Text.Printf.printf with widths and precision (%-10s %6.2f).
  • Padding and alignment: T.justifyLeft, T.justifyRight, T.center; computing column widths.
  • Rendering a grid Map (Int, Int) Char back to text: essential for debugging simulations and for puzzles whose answer appears as letters on a grid.
  • ANSI colors via escape codes ("\ESC[31m") to highlight positions in a grid.
  • Debug.Trace (trace, traceShow, traceShowId) for printing from pure code while debugging.

Bridge

  • printf "%-10s %5d\n" name n ≈ Rust println!("{:<10} {:>5}", name, n), Go fmt.Printf.
  • traceShowId x ≈ Rust dbg!(x).

Exercises

  • Print a word-frequency result as an aligned table with a header and separator line.
  • renderGrid :: Map (Int, Int) Char -> Text, with a highlighted set of positions in color.
  • Animate a simulation from Lesson 7 in the terminal, frame by frame.
  • Add per-part timing and nicely formatted output to the AoC runner.

Check: I can visualize any grid or data structure in the terminal and debug pure code with trace.

Lesson 16: Practice session 4

  • Lesson complete

Exercises

  • 2024 Day 3 (Mull It Over), both parts, using megaparsec.
  • 2023 Day 4 (Scratchcards), both parts: parsing plus a counting recurrence.
  • 2022 Day 7 (No Space Left On Device), both parts: parse a terminal log into a tree or a Map of paths.

Check: My default for any non-trivial input is a megaparsec parser written in minutes.


Unit 5: AoC techniques

Lesson 17: Grids and coordinates

  • Lesson complete

Goals

  • Representations: Map (Int, Int) Char, Set (Int, Int) for sparse grids, Data.Array for dense ones; when to use which.
  • Coordinate helpers: a V2-style point type with Num instance, neighbors (4 and 8), directions, bounds checks.
  • Scanning in a direction until a condition holds; rotating and flipping grids with transpose/reverse.
  • Flood fill and counting connected regions.

Bridge

  • Data.Array ≈ an immutable Vec; (//) copies, so batch updates or use a Map for frequent changes.

Exercises

  • Build AoC.Grid with parseGrid, neighbors4, neighbors8, inBounds, renderGrid.
  • Count the regions of each letter in a grid with flood fill.
  • Find all occurrences of a word in a grid in all 8 directions.

Check: I have a grid module I trust, and grid puzzles feel routine.

Lesson 18: Search: BFS, DFS and Dijkstra

  • Lesson complete

Goals

  • BFS with Data.Sequence as a queue and a Set of visited states.
  • DFS via recursion or a list as a stack; enumerating all paths.
  • Dijkstra with Data.Set (cost, state) as a priority queue (or the psqueues package).
  • Searching over states, not just positions: (position, direction, keysCollected).
  • Generic search functions parameterized by a next :: s -> [(s, cost)] function.

Bridge

  • Data.Sequence ≈ Rust VecDeque, but persistent; Set.deleteFindMin ≈ popping a BinaryHeap (with Reverse).

Exercises

  • Shortest path through a maze with BFS.
  • Count all distinct paths from start to end in a DAG.
  • Shortest path in a weighted grid with Dijkstra, where turning costs extra.
  • Generalize: bfs :: Ord s => (s -> [s]) -> s -> Map s Int.

Check: I can write BFS and Dijkstra from memory, in a generic form I reuse.

Lesson 19: Memoization, recursion and number tricks

  • Lesson complete

Goals

  • Memoization with a Map threaded through recursion, or the State monad for convenience.
  • Lazy memoization: a lazily filled Data.Array or Map whose values reference each other (dynamic programming for free).
  • Integer types: Int vs Integer (overflow!), div/mod vs quot/rem, gcd/lcm.
  • Combinatorics helpers: subsequences, permutations, choosing k elements.
  • Ranges and intervals: merging, splitting, counting overlaps without enumerating elements.

Bridge

  • Lazy array DP ≈ a Python @cache decorator, without mutation.
  • Integer ≈ Python's unbounded int; Int ≈ Rust i64, and it wraps silently on overflow.

Exercises

  • Count ways to build a string from a set of patterns, using memoization.
  • Solve a grid DP (min path sum) with a lazy array.
  • Merge overlapping ranges and count the total length covered.
  • Use lcm to find when several cycles line up.

Check: When part 2 makes brute force impossible, I know which of these tools to reach for.

Lesson 20: Practice session 5

  • Lesson complete

Exercises

  • 2024 Day 4 (Ceres Search), both parts: grid scanning.
  • 2022 Day 12 (Hill Climbing Algorithm), both parts: BFS.
  • 2024 Day 10 (Hoof It), both parts: DFS and counting paths.

Check: I solve medium grid and search puzzles with my own helper modules.


Unit 6: Performance, toolkit and mock days

Lesson 21: Performance

  • Lesson complete

Goals

  • Compiling with -O2; why cabal repl is much slower than the compiled program.
  • Strictness: foldl', Data.Map.Strict, strict record fields (!Int), BangPatterns, seq.
  • Faster containers: Data.IntMap/IntSet, Data.Array.Unboxed, Data.Vector.
  • Encoding coordinates as a single Int key for speed.
  • Basic profiling: +RTS -s for memory and GC stats.

Bridge

  • Space leaks from laziness have no Rust equivalent; strict fields ≈ making sure values are computed, not just promised.

Exercises

  • Take a slow solution from earlier and make it at least 10× faster; record each step's effect.
  • Fix a deliberate space leak found with +RTS -s.

Check: I can explain why a Haskell solution is slow and fix the common causes.

Lesson 22: Personal AoC toolkit

  • Lesson complete

Goals

  • Consolidate reusable modules: AoC.Parse, AoC.Grid, AoC.Search, AoC.Util (counting, windows, ranges).
  • A day template generator: creates DayNN.hs, an example test file and an input placeholder.
  • Tests for the toolkit, so I can trust it under pressure.
  • Optional: fetch inputs automatically with a session cookie (stored outside the repo).

Exercises

  • Review all practice solutions; move repeated helpers into the toolkit.
  • Write the day template generator.
  • Re-solve one earlier puzzle using only the toolkit and time it.

Check: Starting a new day takes one command, and common helpers are one import away.

Lesson 23: Mock puzzle day 1

  • Lesson complete

Goals

  • Simulate a real AoC day: read the puzzle, parse, solve part 1, then part 2, timing each phase.
  • Hints only when stuck for more than 15 minutes.

Exercises

  • 2024 Day 6 (Guard Gallivant), both parts: simulation, cycle detection, performance in part 2.
  • Note the time spent on parsing vs logic vs debugging.

Check: I can complete a mid-level puzzle independently.

Lesson 24: Mock puzzle day 2 and readiness check

  • Lesson complete

Exercises

  • One unseen puzzle from 2023 or 2024 chosen by Claude, at a medium difficulty, solved under the same rules.
  • Retrospective in progress.md: strengths, weak spots, what to review before the event starts.
  • Check that the toolchain, runner, template and toolkit all work on a fresh clone.

Check: I'm ready for Advent of Code.


During Advent of Code

  • Last year's event had 12 puzzles instead of the usual 25. Check the AoC website for this year's format and its rules on using AI tools.
  • Try every puzzle alone first. Claude gives hints only (as defined in CLAUDE.md), never solutions while the puzzle is fresh.
  • After solving, ask for a code review and a reference solution, and add new helpers to the toolkit.
  • Keep logging in progress.md; add new techniques to review.md.

Reference

Essential imports

{-# LANGUAGE OverloadedStrings #-}

import qualified Data.Map.Strict as M
import qualified Data.Set as S
import qualified Data.Sequence as Seq
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Data.List (foldl', sortOn, group, sort, transpose, tails)
import Data.Ord (Down (..), comparing)
import Data.Maybe (mapMaybe, fromMaybe)
import Data.Char (isDigit, isSpace)
import Text.Read (readMaybe)
import Text.Printf (printf)
import Control.Monad (forM_, when, unless)
import Debug.Trace (traceShowId)

Solution template

module AoC.Y2024.Day01 (solve) where

import Data.Text (Text)

data Input = Input -- puzzle-specific

parse :: Text -> Input
parse = undefined

part1 :: Input -> Int
part1 = undefined

part2 :: Input -> Int
part2 = undefined

solve :: Text -> (String, String)
solve raw = let x = parse raw in (show (part1 x), show (part2 x))

Resources

  • Learn You a Haskell for Great Good! (quick syntax refresher)
  • Programming in Haskell by Graham Hutton (chapters on parsers and IO)
  • Hoogle for searching functions by type
  • megaparsec tutorial by Mark Karpov
  • Past AoC puzzles at adventofcode.com, and r/adventofcode for discussions after solving