Capstone — Building a Small Project

Course 2 · Ch 8 — Capstone
Building a Small Project
A type-safe expression interpreter combining nearly every chapter across both Haskell courses

Sixteen chapters, two courses — this capstone builds a small, real expression interpreter touching almost all of it: haskell2-7's own GADT expression language, monadic error handling with Either, a custom typeclass for pretty-printing, and a thin IO shell around a fully pure core.

The Expression Language, Extended

{-# LANGUAGE GADTs #-} data Expr a where -- haskell2-7's own GADT, extended with Div IntLit :: Int -> Expr Int BoolLit :: Bool -> Expr Bool Add :: Expr Int -> Expr Int -> Expr Int Div :: Expr Int -> Expr Int -> Expr Int -- new — genuinely CAN fail at eval time If :: Expr Bool -> Expr a -> Expr a -> Expr a

Div type-checks fine for any two Expr Int — but dividing by zero is a real, runtime possibility no type signature alone can rule out, which is exactly why evaluation needs to be monadic.

Monadic Evaluation — evalM Returns Either String a

evalM :: Expr a -> Either String a -- haskell1-6's own Either, haskell2-3's own >>= chaining evalM (IntLit n) = Right n evalM (BoolLit b) = Right b evalM (Add e1 e2) = do v1 <- evalM e1 v2 <- evalM e2 Right (v1 + v2) evalM (Div e1 e2) = do v1 <- evalM e1 v2 <- evalM e2 if v2 == 0 then Left "division by zero" else Right (v1 `div` v2) evalM (If cond t f) = do c <- evalM cond if c then evalM t else evalM f

Each sub-evaluation is chained with >>=-powered do-notation — if any sub-expression fails, haskell2-3's own short-circuit-on-Left behavior propagates the error automatically, with no manual error-checking at every step.

A Pretty-Printing Typeclass

class Pretty a where -- a real custom typeclass, per haskell1-8 / haskell2-5 pretty :: a -> String instance Pretty (Expr a) where pretty (IntLit n) = show n pretty (BoolLit b) = show b pretty (Add e1 e2) = "(" ++ pretty e1 ++ " + " ++ pretty e2 ++ ")" pretty (Div e1 e2) = "(" ++ pretty e1 ++ " / " ++ pretty e2 ++ ")" pretty (If c t f) = "if " ++ pretty c ++ " then " ++ pretty t ++ " else " ++ pretty f

A Small IO Shell

runExample :: Show a => Expr a -> IO () -- haskell2-4's own pure-core/IO-shell pattern runExample expr = do putStrLn (pretty expr) case evalM expr of Right v -> putStrLn (" = " ++ show v) Left err -> putStrLn (" ERROR: " ++ err) main :: IO () main = do runExample (Add (IntLit 3) (IntLit 4)) runExample (Div (IntLit 10) (IntLit 0)) runExample (If (BoolLit True) (IntLit 1) (IntLit 2))

evalM and pretty are both completely free of IO in their own types — fully testable with ordinary function calls, no mocking required. Only main itself touches IO, exactly haskell2-4's own recommended shape.

Chapter Attribution

Capstone pieceChapter
Expr a GADThaskell2-7
Either-based error handlinghaskell1-6
evalM's do-notation / >>= chaininghaskell2-3
Pattern matching on constructorshaskell1-7
The custom Pretty typeclasshaskell1-8, haskell2-5
Pure core / thin IO shell (main only touches IO)haskell2-4

What's Still Out of Scope

Honestly: no real parser or lexer — expressions are built directly in Haskell code, not typed as text by a user, so this isn't a genuine interactive REPL. No monad transformers used here at all, despite haskell2-6 covering them — evalM's own error handling only needs plain Either, a deliberate scope decision, not an oversight. No phantom types beyond the GADT's own per-constructor typing. This capstone proves the pieces fit together, not that the result is a production interpreter.

A capstone's value is in the seams, not the size
This project is deliberately small — the point was never scale, it was confirming that GADTs, monadic error handling, custom typeclasses, and a disciplined pure-core/IO-shell split genuinely compose cleanly together in real code.
This is a teaching capstone, not a template for a real interpreter
A real interpreter would need actual parsing, a proper error-reporting story (source locations, not just a message string), and tests — treat this as proof the language features fit together, not as production-ready code to copy.

Coding Challenges

Challenge 1

Add a new GADT constructor Mul :: Expr Int -> Expr Int -> Expr Int alongside Add, update evalM and pretty to handle it, and test it with a small expression combining Add and Mul.

📄 View solution
Challenge 2

Write a nested expression that divides by zero INSIDE a larger Add expression (e.g. Add (IntLit 5) (Div (IntLit 10) (IntLit 0))), run it through evalM, and confirm the error propagates out of the whole expression correctly, explaining why in a comment.

📄 View solution
Challenge 3

Write a short paragraph (as a comment) explaining what would need to change in this capstone if it needed to ALSO thread a variable-count "operations performed" counter through evaluation, referencing haskell2-6's own StateT material directly.

📄 View solution

Chapter 8 Quick Reference — Haskell Track Complete

  • A GADT lets each constructor declare its own specific return type, enabling a genuinely type-safe expression language (haskell2-7)
  • evalM's Either-based do-notation chains sub-evaluations, short-circuiting automatically on failure (haskell1-6, haskell2-3)
  • A custom Pretty typeclass demonstrates real, practical ad-hoc polymorphism (haskell1-8, haskell2-5)
  • evalM and pretty stay completely free of IO — only main touches it, haskell2-4's own recommended shape
  • Monad transformers, real parsing, and phantom types are honestly named as still out of scope
  • Both Haskell courses are now complete — 16 chapters total, framed throughout as the real origin of ideas already met on this site as Rust's Option/Result/traits.