25 KiB
Haskell Syllabus: Advent of Code 2026 Edition
Overview
Goal: Solve Advent of Code 2026 in Haskell. Focus on four core skills:
- Simple IO: reading and parsing string data from files
- String parsing: parsing and managing strings
- Data transformation: mapping, filtering, folding, grouping
- 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
- Warm-up (5–10 min): review items due in
review.md. - Concepts (20–30 min): the lesson's topics, explained against Rust/OCaml where it helps.
- Exercises (30–45 min): stubs with failing tests in
src/Exercises/LessonNN.hs. I write the code; Claude gives hints, not solutions. - Wrap-up (5 min): check criterion, update
progress.mdandreview.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
hlintandormolu; 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 testgreen with one trivial test. - Add
text,containersandmegaparsecas dependencies and import each once to confirm they build. - Look up
[a] -> Int,(a -> b) -> [a] -> [b]andString -> [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). wherevslet, 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; derivingShow,Eq,Ord.MaybeandEitheras the everyday error types.
Bridge
Maybe a≈ RustOption<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(fromData.Function) ≈ Elixir/OCaml|>.
Exercises
- Model a
Dirtype withturnLeft,turnRight,opposite, andmove :: Dir -> (Int, Int) -> (Int, Int). - Write
safeDiv :: Int -> Int -> Maybe IntandparseDir :: 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 Addplus an identity;<>combines,memptyis the empty value.- Laziness ≈ Rust iterators or Python generators, except it's the default everywhere.
Exercises
- Derive
EnumandBoundedforDirand use them to rewriteturnRightwithout listing every case. - Write a
Monoidinstance for aStatstype (count, sum, min, max) and combine a list withmconcat. - Predict, then verify in GHCi:
take 5 (filter even [1..]),length [undefined, undefined],foldl (+) 0 [1..10^7]vsfoldl' (+) 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,wordsandreadfor now. - Establish the solution template:
parse,part1,part2,mainreadinginputs/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/filterchains; comprehensions for generating combinations and pairs. Data.List:sort,sortOn,sortBy,group,groupBy,nub,partition,transpose,tails,inits,isPrefixOf,isInfixOf.Data.Ord:comparing,Downfor descending sorts.
Bridge
foldr/foldl'≈ OCamlList.fold_right/List.fold_left, Rust.fold().sortOn (Down . snd)≈ Rustsort_by_key(|x| Reverse(x.1)).[(x, y) | (x:ys) <- tails xs, y <- ys]≈ all unique pairs, like Pythonitertools.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)]usinggroup. - 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 asM):fromList,fromListWith,insertWith,adjust,alter,lookup,findWithDefault,toList,foldrWithKey,unionWith.Data.Set: membership, union, intersection, difference;Data.IntMapforIntkeys.- Counting and grouping idioms: frequency tables, group-by-key.
- Functor and
fmap/<$>onMaybe,Either, lists. traverse/mapMto turn[Maybe a]intoMaybe [a](fail fast);mapMaybeandcatMaybesto drop failures.
Bridge
M.fromListWith (+)≈ Rustentry().or_insert(0) += 1or PythonCounter.traverse parse xs≈ Rustxs.iter().map(parse).collect::<Result<Vec<_>, _>>().>>=onMaybe≈ Rust.and_then();<$>≈.map().
Exercises
- Word frequency: count words in a string, case-insensitive, sorted by count desc.
- Group
(department, employee)pairs intoMap 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. scanlfor every intermediate state;iterateplus!!/takeWhilefor repeated steps.untiland 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
Statemonad 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 Ruststd::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:
zipWithand 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
IOtype;do-notation;<-vsletinsidedo;pure/return. - Reading input:
readFile,getContents,interact; writing:putStrLn,print. - Command-line arguments with
getArgs; exiting withSystem.Exit. mapM_,forM_,when,unlessfromControl.Monad.- An AoC runner:
cabal run aoc -- 2024 1picks the day's module and input file.
Bridge
do≈ Rust?-chained code or OCamllet*;<-"unwraps" a monadic value.interact :: (String -> String) -> IO ()≈ a whole stdin-to-stdout filter in one line.
Exercises
wcclone: 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.TextandData.Text.IO(qualified asT/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,
trywithIOException, turning them intoEither.
Bridge
String≈ a linked list of chars;Text≈ RustString/&str.try (readFile p) :: IO (Either IOException String)≈ Rustfs::read_to_string(p)returningResult.
Exercises
- Port the runner to
Textinput. - Parse blank-line-separated groups of numbers into
[[Int]]. - Read a
key = valuefile intoMap 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/spanon a delimiter. readMaybefromText.Read;Data.Text.Read(decimal,signed) forText.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) Charwithzip [0..].
Bridge
readMaybe s :: Maybe Int≈ Rusts.parse::<i32>().ok().- Grid parsing with nested
zip [0..]≈ Pythonenumerateinsideenumerate.
Exercises
ints :: Text -> [Int]: extract all (possibly negative) integers from"x=-3, y=12 to 7".- Parse lines like
move 3 from 1 to 2into aCommandtype. - 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.
Applicativeoperators that make parsing readable:<$>,<*>,*>,<*,<|>.megaparsecbasics:Parsec Void Text,parse,errorBundlePretty,char,string,space,L.decimal,L.signed,sepBy,some,many.- Brief look at
ReadP(inbase, no dependency) for quick one-offs.
Bridge
- ≈ Rust
nomorchumsky, OCamlangstrom. Point <$> int <* char ',' <*> int≈ "parse an int, skip a comma, parse an int, build a Point".
Exercises
- Parse
"(3,4)"intoPoint 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
errorBundlePrettyoutput.
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,
tryand 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.Parsemodule 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
showvs custom rendering;Text.Printf.printfwith widths and precision (%-10s %6.2f).- Padding and alignment:
T.justifyLeft,T.justifyRight,T.center; computing column widths. - Rendering a grid
Map (Int, Int) Charback 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≈ Rustprintln!("{:<10} {:>5}", name, n), Gofmt.Printf.traceShowId x≈ Rustdbg!(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
Mapof 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.Arrayfor dense ones; when to use which. - Coordinate helpers: a
V2-style point type withNuminstance, 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 immutableVec;(//)copies, so batch updates or use aMapfor frequent changes.
Exercises
- Build
AoC.GridwithparseGrid,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.Sequenceas a queue and aSetof 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 thepsqueuespackage). - Searching over states, not just positions:
(position, direction, keysCollected). - Generic search functions parameterized by a
next :: s -> [(s, cost)]function.
Bridge
Data.Sequence≈ RustVecDeque, but persistent;Set.deleteFindMin≈ popping aBinaryHeap(withReverse).
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
Mapthreaded through recursion, or theStatemonad for convenience. - Lazy memoization: a lazily filled
Data.ArrayorMapwhose values reference each other (dynamic programming for free). - Integer types:
IntvsInteger(overflow!),div/modvsquot/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
@cachedecorator, without mutation. Integer≈ Python's unboundedint;Int≈ Rusti64, 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
lcmto 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; whycabal replis 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
Intkey for speed. - Basic profiling:
+RTS -sfor 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 toreview.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