quaalude

A replacement Prelude for Haskell, written for Advent of Code. It re-exports Relude and dozens of other libraries and adds thirty modules of its own (collections, grids, coordinates, parsing, search, Z3, tracing) behind a dense Unicode notation, so that a solution needs no imports and reads close to the way you would write it on paper.

It is a personal tool and deliberately opinionated: symbols over words, overlapping instances, type-level tricks. This page explains how it works and how it reads, with every example run in GHCi against the compiled library, and ends with a reference generated from it.

A solution, read slowly

This is the whole of one solution, 2024's day 6: walk a guard around a grid, turning right at obstacles, and (part 2) count the places where one new obstacle would trap it in a loop. There are no imports: everything comes from quaalude, installed as the Prelude. Hover over a name for its type.

module Day6 (part1, part2) where

walk :: Grid Char -> Ѓ (Set (ℤ₆₄ × ℤ₆₄)) (?)
walk g = go (∅) def =<< (gridFindOne '^' g)
  where
    go seen d c
      | (c, d) ∈ seen = Nothing
      | c ∉ g = Just $ setMap fst seen
      | g |? move @ℤ₆₄ d 1 c == Just '#' = go seen (turnCW d) c
      | otherwise = go ((c, d) |-> seen) d (move @ℤ₆₄ d 1 c)

part1 :: Ѓ ℤ (?)
part1 = size <$> walk $(grid 6)

part2 :: Ѓ ℤ (?)
part2 =
  let g = $(grid 6)
      walk' = (((as @ℤ . isNothing . walk . (g ||.) . (,'#')) >>>) . (+))
   in foldl' walk' (-1) <$> (unSet <$> walk g)

$(grid 6) is Template Haskell: it reads day 6's input when the program is compiled and splices it in as a Grid Char, so the solution itself contains no file handling. Ѓ ℤ (?) is a type family that means Maybe ℤ. walk returns Nothing when the guard revisits a position facing the same way, which is how part 2 detects a loop.

Part 2 tries an obstacle at each position on the original path (only those can change the route) and counts the walks that loop: as @ℤ . isNothing turns "looped" into 1 or 0. The fold starts at −1 for a subtle reason: an obstacle placed on the guard's starting cell overwrites the ^ that marks it, so gridFindOne '^' finds nothing, walk returns Nothing, and that case would be counted as a loop.

This solution in the Advent of Code viewer.

Every quaalude name in this solution, with its type
NameTypeModule
∈a -> b -> BoolCollection
sizeIntegral i => a -> iCollection
|->a -> f a -> f aCollection
asfrom -> toUtil
|?f k v -> k -> Maybe vCollection
gridInt -> Q ExpTH
move(Num c, Num i, Coord' i c (c, c)) => Dir2 -> i -> (c, c) -> (c, c)Coord
∉a -> b -> BoolCollection
∅Monoid a => aCollection
gridFindOneGriddable Identity g k a => a -> g k a -> Maybe kGrid
unSetSet a -> [a]Collection
turnCWd -> dCoord
setMapOrd b => (a -> b) -> f a -> f bCollection
||.Griddable Identity g k a => g k a -> (k, a) -> g k aGrid

Installing it as the Prelude

quaalude is not on Hackage. The Advent of Code years that use it carry it as a Git submodule and list it in cabal.project; Cabal's mixins then hide base's Prelude and rename Quaalude to Prelude, so every module in the package gets it without an import.

# cabal.project
packages:
  ./x2024.cabal
  ./lib/quaalude
# package.yaml
dependencies:
  - name: base
    mixin:
      - hiding (Prelude)
  - name: quaalude
    mixin:
      - hiding (Quaalude)
      - (Quaalude as Prelude)

The library turns on 55 language extensions by default (OverloadedStrings, UnicodeSyntax, DataKinds, TypeFamilies …); a project using it copies the same list. It needs GHC 9.8 or newer.

Notation

A few of the aliases
numbersℤ Integer · ℤ₆₄ Int · ℕ Natural · ℚ Rational · ℝ Double · ℂ Complex
tuplesℤ² … ℤ¹⁰ tuples of ℤ · a × b pairs
others𝔹 Bool · 𝕊 String · 𝕋 Text
logic≡ ≢ ≤ ≥ ∧ ∨
functions∘ compose · ⋅ multiply · # pure · ⊥ undefined
lists(<>!) mconcat · (🎜) sort · (🎝) sort descending

Most of the notation is aliases for things Haskell already has, chosen so that types and expressions read like mathematics. They are real type synonyms and functions, so GHCi expands them:

λ :kind! ℤ
ℤ :: *
= Integer
λ :kind! ℤ₆₄
ℤ₆₄ :: *
= Int
λ :kind! ℤ²
ℤ² :: *
= (Integer, Integer)
λ :kind! Ѓ ℤ (?)
Ѓ ℤ (?) :: *
= Maybe Integer

One vocabulary for every collection

Haskell's containers each come with their own functions: Set.member, Map.lookup, Seq.index. quaalude puts one set of names over all of them with type classes: mk builds any collection from a list, un lists any collection, co converts between them, and operators such as ∈, |? (look up), |! (index), |. (set a key) and |-> (insert) work on whatever they are given.

λ mk [3, 1, 2, 1] :: Set ℤ
fromList [1,2,3]
λ un (mk [3, 1, 2, 1] :: Set ℤ)
[1,2,3]
λ co [(1, 'a'), (2, 'b')] :: Map ℤ Char
fromList [(1,'a'),(2,'b')]
λ 3 ∈ (mk [1, 2, 3] :: Set ℤ)
True
λ 3 |-> (mk [1] :: Set ℤ)
fromList [1,3]
λ size (mk "hello" :: Set Char) :: ℤ
4
λ (mkMap [(1, 'a')] :: Map ℤ Char) |? 2
Nothing
λ (mkMap [(1, 'a')] :: Map ℤ Char) |! 1
'a'
λ (mkMap [(1, 'a')] :: Map ℤ Char) |. (2, 'b')
fromList [(1,'a'),(2,'b')]

The price is type annotations. With this many overlapping instances, GHC often cannot work out which collection is meant, so code names it (:: Set ℤ), and the type of a container changes which builder works: mk builds the f a shapes (lists, sets, sequences, vectors), while a map needs mkMap or co.

Grids and coordinates

Grids are everywhere in Advent of Code, so Griddable is the largest class in the library. It is parameterised by a monad, which lets the same operations run over immutable storage and over mutable arrays in ST or IO when a simulation needs speed: Map, IntMap, HashMap, nested Vectors, immutable arrays, and mutable IOArray, STArray, STUArray, STVector and IOVector grids. Coordinates are (x, y) pairs; gridFindOne returns the first match in that order, column before row.

Cells can be any type with a GridCell instance: Char, DotHash, digits, or Cell "…", a cell type restricted at the type level to the characters in its string.

λ let g = readGrid "..#\n#..\n" :: Grid Char
λ g ||? (2, 0)
Just '#'
λ gridFindOne '#' g
Just (0,1)
λ maxXY g
(2,1)
λ turnCW DirUp
→
λ move DirUp (2 :: Int) (5, 5 :: Int)
(5,3)
λ manhattan (1, 2) (4, 6 :: Int)
7

Parsing and small helpers

Most puzzle input is numbers in some text, so numbers is a parser that finds them and |- runs a parser on a string and returns its result. Parsec is re-exported for anything harder, and Quaalude.Megaparsec adds a few helpers for Megaparsec. The rest of this block is the small change of everyday code: applying a two-argument function to a pair, duplicating a value, converting with as, sorting, and a conditional that reads left to right.

For search, Quaalude.LossQ provides priority queues ordered by a loss function, for Dijkstra and A*-style searches; monad-memo is re-exported for memoised recursion; and Quaalude.Solve wraps the Z3 solver, so a puzzle that is really a system of constraints can be handed to Z3 and the answer read back by type.

λ "move 10 to -3, then 42" |- numbers :: [ℤ]
[10,-3,42]
λ both (+ 1) (1, 2)
(2,3)
λ (+) $@ (2, 3)
5
λ dup 'x'
('x','x')
λ (<>!) ["ab", "cd"]
"abcd"
λ as @ℤ True
1
λ (🎜) [3, 1, 2]
[1,2,3]
λ (🎝) [3, 1, 2]
[3,2,1]
λ (2 > 1) ??? "yes" $ "no"
"yes"

The names you will meet

Counted across the 2024 and 2025 solutions, which use quaalude as their Prelude, by scripts/site/usage.py. aoc, which embeds a day's input at compile time, is in almost all of them.

The 30 most widely used of the 239 quaalude names in the 37 solutions
NameSolutionsTypeModule
aoc30Int -> Q ExpTH
∘17(b -> c) -> (a -> b) -> a -> cAlias
∈16a -> b -> BoolCollection
≡14Eq a => a -> a -> BoolAlias
<>!14Monoid a => [a] -> aAlias
#13Applicative m => a -> m aAlias
size12Integral i => a -> iCollection
|!12f k v -> k -> vCollection
|-12String -> Parser a -> aUtil
⋅12Num a => a -> a -> aAlias
|->11a -> f a -> f aCollection
both10Bifunctor f => (a -> b) -> f a a -> f b bUtil
number9(Read a, Num a) => Parser aUtil
$@9(a -> b -> c) -> (a, b) -> cTuple
as9from -> toUtil
?8a -> QuestionableF a -> QuestionableFR aAlias
cs8[(a, Char)]Grid
|?7f k v -> k -> Maybe vCollection
grid7Int -> Q ExpTH
mk7[a] -> f aCollection
nub6Eq a => [a] -> [a]Collection
neighs6a -> g -> f aCoord
mk₁6Mkable f a => a -> f aCollection
∧6Bool -> Bool -> BoolAlias
run6f a -> aUtil
numbers6(Read a, Num a) => Parser [a]Util
|.6f k v -> (k, v) -> f k vCollection
!!6f a -> i -> aCollection
???6Bool -> a -> a -> aAlias
□6(SChar c :< SymSChars cs, KnownChar c) => SChar c -> Cell csGrid

What it costs

  • Dependencies. 58 packages, including bindings to the Z3 C library, all re-exported. A fresh build has to compile or fetch all of them before the first solution builds.
  • Type errors. Overlapping and polymorphic instances make inference fragile: a missing annotation can produce an ambiguity error that names the class rather than the mistake, and some names behave differently on different containers.
  • Reading. The notation is fast to write and compact to read once learned, but a newcomer has a large vocabulary to learn, much of it Unicode that needs an editor set up to type it.
  • Scope. It is made for puzzles: partial functions are used freely, and nothing is promised about stability. The Advent of Code viewer shows where it helps and where it obscures, solution by solution.

Reference

The reference lists every module's exports as GHCi's :browse prints them for the compiled library, with a filter for names and types.