Algebraic Data Types and Pattern Matching

Consider writing a function that evaluates simple arithmetic expressions. These expressions contain numbers, additions, and conditionals. In a language without dedicated support for such shapes, a programmer reaches for tagged unions. These are built from raw pointers, integer tags, and unchecked casts. This is a notoriously bug-prone corner of C-like languages. The compiler cannot tell whether every case of the tag has been handled. A stray cast silently corrupts memory when the tag is wrong. Algebraic data types (ADTs) solve exactly this problem. They let a language describe "one of several shapes of data" as a first-class type. Pattern matching gives a syntax for taking such a value apart. The compiler can check this syntax for completeness. This pairing — sum types plus exhaustive pattern matching — is one of the most consequential ideas in modern programming language design. It underlies the `enum` types of Rust and Swift, the `data` declarations of Haskell, the discriminated unions of TypeScript and OCaml, and the case classes of Scala. Compilers that support ADTs can statically guarantee that a match covers every constructor. This converts what would be a runtime `NullPointerException` or an unhandled `switch` fallthrough into a compile-time error. This single feature has measurably reduced a whole class of production bugs in languages that adopted it. The idea rests on two dual notions from type theory. Product types bundle several pieces of data together, like a tuple or struct. Sum types offer a choice between several alternative shapes, like a tagged union. An algebraic data type is built from sums and products of other types, hence "algebraic." The type-formation rules mirror the algebraic laws of addition and multiplication. Pattern matching is the elimination principle dual to constructors. Constructors build values, and pattern matching destructs them. The compiler enforces that every constructor is accounted for. This article builds ADTs from the Simply-Typed Lambda Calculus up. It treats sums and products as first-class type formers. It defines pattern matching as a typed, exhaustiveness-checked case analysis. It proves why the mechanism is both safe and efficient. It assumes familiarity with typed lambda calculi and Parametric Polymorphism. It also sets up ideas that recur later. These ideas reappear when studying Recursion and Higher-Order Functions and later Monads: An Introduction. There, `Maybe`/`Option` and `Either` — themselves ADTs — become the central data types.

Key takeaways
  • Product types (tuples, records) bundle values; sum types (tagged unions) offer alternatives — ADTs combine both.
  • Constructors are typed functions that build sum-type values, each tagged with its variant.
  • Pattern matching is the dual operation: it destructs a value by case analysis on its constructor.
  • Exhaustiveness checking lets the compiler prove, at compile time, that every case is handled.
  • ADTs replace error-prone tagged unions and null checks with statically verified alternatives.
  • Recursive ADTs (lists, trees) plus structural recursion give a principled foundation for recursive functions.
The core insight

Algebraic data types let a compiler enumerate every possible shape of a value in advance, so pattern matching can be checked for completeness the same way a type checker checks for well-typedness.

The idea

A product type `A × B` (written `(A, B)` or as a record `{fst: A, snd: B}`) represents a value that simultaneously contains an `A` and a `B`. Its introduction rule is pairing: from `a : A` and `b : B` we build `(a, b) : A × B`. Its elimination rule is projection: from `p : A × B` we extract `fst(p) : A` and `snd(p) : B`. Records and structs generalize this to named fields; tuples are the anonymous case. The name "product" comes directly from counting: if `A` has `m` inhabitants and `B` has `n`, then `A × B` has exactly `m·n` inhabitants — one for each pairing, just as in ordinary multiplication. A sum type `A + B` (also called a tagged union, disjoint union, or variant type) represents a value that is either an `A` or a `B`, tagged so the two cases are distinguishable even when `A` and `B` overlap or coincide. Its introduction rules are the two constructors `inl : A → A + B` and `inr : B → A + B` (traditionally "inject left" / "inject right"; in surface syntax these become named constructors like `Some`/`None` or `Leaf`/`Node`). Its elimination rule is case analysis: to consume a value of type `A + B` you must supply a handler for both possibilities. Counting again: if `A` has `m` inhabitants and `B` has `n`, then `A + B` has `m + n` — one for each tagged copy, matching ordinary addition. This numeric analogy is why the whole family is called algebraic: type formation obeys laws such as `A × (B + C) ≅ A × B + A × C` (distributivity) and `A + 0 ≅ A` (where `0` is the empty/void type, the additive identity), directly paralleling ring axioms. A general algebraic data type declaration introduces a new named type together with a finite list of constructors, each constructor taking a (possibly empty) tuple of argument types — so each constructor is itself a product, and the type as a whole is a sum of those products. For example, ``` data Shape = Circle(radius: Float) | Rectangle(width: Float, height: Float) | Triangle(a: Float, b: Float, c: Float) ``` declares `Shape` as `Circle-args + Rectangle-args + Triangle-args`, where each variant's arguments form a product. Constructors are ordinary typed functions — `Circle : Float → Shape` — except that they are injective and disjoint: no two different constructors ever produce equal values, and each constructor is determined by its arguments. This injectivity/disjointness pair is what lets pattern matching work: given a `Shape` value, we can always ask "which constructor produced this, and what were its arguments?" and get a unique answer. A `match` (or `case`, or `switch` in ADT-aware languages) expression enumerates patterns — one per constructor, optionally with sub-patterns for nested ADTs and variables to bind constructor arguments — and the compiler's exhaustiveness checker verifies that the patterns cover every possible constructor of the scrutinee's type, with no case left unhandled and (for well-designed languages) no unreachable case left dead. Recursive ADTs, where a constructor's arguments mention the type being defined (e.g., `List(a) = Nil | Cons(a, List(a))`), give the type-theoretic foundation for inductively defined data. Pattern matching on such types, combined with structural recursion, is precisely the mechanism used to define functions over lists, trees, and other recursive structures — the topic taken up under Recursion and Higher-Order Functions.

How it works

Typing constructors. For a data declaration ``` data T(α₁,...,αₖ) = C₁(τ₁₁,...,τ₁ₙ₁) | C₂(τ₂₁,...,τ₂ₙ₂) | ... | Cₘ(τₘ₁,...,τₘₙₘ) ``` (possibly parametrically polymorphic in type variables `α₁,...,αₖ`, connecting to Parametric Polymorphism), the type checker installs each `Cᵢ` as a function ``` Cᵢ : τᵢ₁ → τᵢ₂ → ... → τᵢₙᵢ → T(α₁,...,αₖ) ``` This is exactly a curried constructor function, typed like any other function in the simply typed lambda calculus, except it is marked as a data constructor so the exhaustiveness checker (below) can enumerate `{C₁,...,Cₘ}` as the complete set of ways to build a `T`.

Typing and checking a match. A match expression ``` match e { p₁ -> e₁ | p₂ -> e₂ | ... | pₖ -> eₖ } ``` is typed by: (1) inferring the type `T` of the scrutinee `e`; (2) checking each pattern `pᵢ` against `T`, extending the local typing context with any variables bound by `pᵢ`; (3) checking each branch body `eᵢ` at some common result type `σ`, under its extended context; (4) running exhaustiveness and (5) redundancy checks over the pattern list. If all pass, the whole match has type `σ`.

Exhaustiveness checking is the algorithmic heart of the mechanism. The standard technique (used in ML-family and Haskell compilers) maintains a set of uncovered pattern spaces, represented compactly as a list of pattern vectors with wildcards, and processes the match's patterns top to bottom, subtracting each pattern's coverage from the uncovered set: ``` function CheckExhaustive(scrutineeType T, patterns [p1, ..., pk]): uncovered := { Wildcard } # initially: "any value of type T" is uncovered for pi in patterns: if uncovered is empty: report "pattern pi is unreachable" # redundancy check newUncovered := {} for u in uncovered: newUncovered += Subtract(u, pi) # part of u not matched by pi uncovered := newUncovered if uncovered is not empty: witness := PickExample(uncovered) report "non-exhaustive match: missing case, e.g. " + witness else: report "exhaustive" function Subtract(spacePattern u, matchPattern p):

# u and p are pattern trees over the same constructor tree;

# returns the part of u's coverage not covered by p, as a set (list) of pattern trees if p is Wildcard or Variable: return {} # p covers everything u covers if p is Constructor(C, subpatterns) and u is Wildcard:

# expand u into one sub-case per constructor of T result := {} for each constructor Cj of T (Cj != C): result += { Constructor(Cj, Wildcard,...,Wildcard) } result += { Constructor(C, Wildcard,...,Wildcard) minus-matched-by(p) via recursive Subtract on arguments } return result if p is Constructor(C, subpatterns) and u is Constructor(C, subU) with same C: return CartesianSubtract(subU, subpatterns) # recurse component-wise if p is Constructor(C, ...) and u is Constructor(C', ...) with C != C': return { u } # p cannot touch this case at all ``` Intuitively, `uncovered` tracks "the region of the value space not yet proven reachable by an earlier pattern," starting as the whole type and shrinking (constructor by constructor, argument by argument) as each pattern is processed. If anything remains uncovered at the end, the compiler has found a concrete counterexample value — a witness — that no pattern matches, and it reports that value as a missing case. If, when processing pattern `pᵢ`, the uncovered set is already empty, then `pᵢ` can never fire and is flagged as dead code — a genuinely useful diagnostic in its own right.

Compiling a match to code. Once type-checked, a match compiles to a decision tree or backend switch: at runtime, each value carries a small integer or pointer tag identifying its constructor (plus, inline or boxed, its constructor arguments). Compilation dispatches on this tag — typically via a jump table for a match with many constructors, giving `O(1)` dispatch — then binds the pattern variables to the corresponding fields and evaluates the matched branch. Nested patterns (e.g., matching on `Cons(Leaf, rest)`) are compiled via well-studied pattern-match compilation algorithms (Wadler's or the Maranget "compiling pattern matching to good decision trees" approach) that share tests between overlapping patterns to avoid re-scrutinizing the same subterm twice. ```

# Runtime representation and dispatch, sketched struct Value { tag: Int # which constructor, e.g. 0=Nil, 1=Cons fields: Array # constructor arguments, empty for nullary constructors } function evalMatch(v: Value, branches: Array<(tag: Int, arity: Int, body: Fn)>): for (t, arity, body) in branches: if v.tag == t: return body(v.fields[0], ..., v.fields[arity-1]) # bind and evaluate error("unreachable: exhaustiveness checker guarantees this never triggers") ``` The final `error` line is the operational payoff of exhaustiveness checking: it is provably dead whenever the static checker accepted the match, so production compilers either omit it or compile it to a trap instruction rather than a real branch.

Figure
Sum-of-products layout: `data Shape = Circle(Float) | Rectangle(Float,Float) | Triangle(Float,Float,Float)`
type Shape (sum of 3 constructors) tag ∈ {0=Circle, 1=Rectangle, 2=Triangle} (dispatch key, O(1) lookup) Circle radius : Float product size 1 Rectangle width, height : Float product size 2 Triangle a, b, c : Float product size 3 match shape { Circle(r)->A | Rectangle(w,h)->B | Triangle(a,b,c)->C } uncovered after Circle: {Rectangle, Triangle} uncovered after Rectangle: {Triangle} uncovered after Triangle: {} → exhaustive ✓, compiles to O(1) jump table

Complexity & correctness

Type checking cost. Checking a single constructor application `Cᵢ(e₁,...,eₙᵢ)` costs the same as checking any `n`-ary function application: `O(n)` recursive type checks against the declared argument types, so this step contributes no asymptotic overhead beyond ordinary function application in the simply typed lambda calculus.

Exhaustiveness checking cost. Let a match have `k` patterns over a scrutinee type with maximum constructor arity `a` and nesting depth `d` (how deeply patterns nest sub-patterns). The uncovered-set algorithm processes patterns one at a time, and in the worst case the uncovered set can grow to size proportional to the product of constructor counts along each nested position — but in practice, and in all standard formulations (Maranget's algorithm, used in OCaml and Rust), the check runs in time polynomial in the size of the pattern matrix: `O(k · d · a)` for well-structured matches, since each pattern only needs to be compared against the current frontier of uncovered constructor combinations, and each comparison walks the pattern tree once. Pathological cases with many independent nested sum types can exhibit combinatorial blowup in the number of missing-case witnesses reported, but the yes/no exhaustiveness decision itself is decidable and tractable for all patterns arising from realistic ADTs, because constructor sets are finite and fixed by the type declaration — this finiteness is exactly what makes the check terminate, unlike exhaustiveness questions over unbounded domains like integers.

Match compilation and dispatch cost. Compiling `k` patterns into a decision tree costs `O(k · d)` in the size of the generated code (each pattern contributes at most one test per nesting level), and the resulting runtime dispatch is `O(1)` for a top-level tag switch (jump table) plus `O(d)` for extracting nested fields — matching the cost of an ordinary field access or array index, i.e. no asymptotic overhead versus hand-written tagged-union code with manual `if`/`else` tag checks.

Correctness argument. The key safety property is: > Progress-and-preservation for match: if `e : T` is a well-typed value (a constructor application `Cᵢ(v₁,...,vₙᵢ)`) and `match e {p₁->e₁,...,pₖ->eₖ}` passed exhaustiveness checking, then some `pⱼ` matches `e`, and the corresponding branch is well-typed at the match's result type. The proof is a direct consequence of two invariants maintained by the algorithm above: 1. Soundness of `Subtract`: at every step, `uncovered` exactly denotes the set of values of type `T` matched by none of `p₁,...,pᵢ` so far — proved by induction on the pattern list, using the fact that `Subtract(u, p)` is defined by case analysis that exactly mirrors the constructor structure of `T` (each constructor is either fully consumed by `p`, fully excluded, or its own sub-arguments recursively subtracted). 2. Completeness of the constructor set: because `T`'s constructors `C₁,...,Cₘ` are declared exhaustively and are pairwise disjoint and injective (a metatheoretic property enforced by the type system, not by the programmer), every value of type `T` is `Cᵢ(v₁,...,vₙᵢ)` for exactly one `i` — this is the "no junk, no confusion" property standard in the theory of inductive/algebraic datatypes. Combining (1) and (2): if `uncovered = ∅` after processing all patterns, then by (1) no value of type `T` is left unmatched, and by (2) every runtime value genuinely has some constructor form, so the runtime dispatch in `evalMatch` is guaranteed to find a matching branch — hence the `error("unreachable")` line is dead code whenever the static check succeeds. This is a genuine proof by structural induction on the finite constructor set, not merely a claim: it is exactly analogous to proving a `switch` over an `enum` total once every enum tag has a case, except ADTs generalize this to arbitrarily nested, recursive, and parametrically polymorphic shapes.

Tradeoffs. Exhaustiveness checking requires the compiler to know the closed set of constructors for a type ahead of time; this is why "open" extensible unions (as in object-oriented subclassing, or JavaScript's untagged values) cannot get the same static guarantee without extra machinery (sealed traits, visitor patterns, or explicit "open union" library encodings). Deeply nested patterns over many-constructor types can also produce exponentially many warning messages (though not exponential checking time) if a language reports every missing combination individually rather than a minimal counterexample — real compilers mitigate this by reporting only one witness per top-level match, as sketched in the pseudocode above.

Worked example

Take a small recursive ADT for arithmetic expressions and a function evaluating them, then trace both type checking and exhaustiveness checking concretely. ``` data Expr = Num(Int) | Add(Expr, Expr) | If(Expr, Expr, Expr) # nonzero test: If(cond, then, else) function eval(e: Expr) -> Int: match e { Num(n) -> n Add(l, r) -> eval(l) + eval(r) If(c, t, f) -> if eval(c) != 0 then eval(t) else eval(f) } ``` Consider evaluating `Add(Num(3), If(Num(0), Num(10), Num(20)))`.

StepExpressionConstructor matchedBound variablesResult
1`Add(Num(3), If(Num(0), Num(10), Num(20)))``Add(l, r)``l = Num(3)`, `r = If(Num(0),Num(10),Num(20))`recurse into `eval(l) + eval(r)`
2`eval(Num(3))``Num(n)``n = 3`returns `3`
3`eval(If(Num(0), Num(10), Num(20)))``If(c,t,f)``c=Num(0)`, `t=Num(10)`, `f=Num(20)`evaluate `c` first
4`eval(Num(0))``Num(n)``n = 0`returns `0`; since `0 != 0` is false, branch chooses `f`
5`eval(Num(20))``Num(n)``n = 20`returns `20`
6combine step 1——`3 + 20 = 23`

Now trace the exhaustiveness checker on the `match` inside `eval`, following the pseudocode: 1. `uncovered = {Wildcard}` (any `Expr` at all). 2. Process `Num(n)`: `Wildcard` expands over `Expr`'s three constructors; the `Num`-shaped part is fully consumed (bound by variable `n`), leaving `uncovered = {Add(Wildcard,Wildcard), If(Wildcard,Wildcard,Wildcard)}`. 3. Process `Add(l, r)`: matches the `Add` case exactly (both sub-patterns are variables, hence wildcards), removing it: `uncovered = {If(Wildcard,Wildcard,Wildcard)}`. 4. Process `If(c, t, f)`: matches the remaining `If` case exactly: `uncovered = {}`. 5. Final `uncovered` is empty → exhaustive, no runtime fallthrough possible, compiled to a 3-way jump table indexed by tag. Now suppose a programmer forgets the `If` case: ``` function badEval(e: Expr) -> Int: match e { Num(n) -> n Add(l, r) -> badEval(l) + badEval(r) } ``` Re-running the check: after step 3 above, `uncovered = {If(Wildcard,Wildcard,Wildcard)}`, and there are no more patterns to process. The checker reports this residual as a concrete witness — e.g. `If(Num(0), Num(0), Num(0))` — and the compiler rejects the function before it ever runs, in contrast to a language with plain `switch`/`instanceof` chains, where the analogous mistake compiles cleanly and only manifests as a crash (or worse, silently wrong behavior from a default branch) when an `If` node is finally evaluated in production.

Common pitfalls & traps

PitfallWhy it happensFix
Treating pattern variables as constantsA lowercase identifier in a pattern (e.g. `n` in `Num(n)`) always binds, never compares by valueUse guard clauses (`Num(n) if n == 0 -> ...`) or literal patterns to test specific values
Adding a new constructor without updating existing matchesClosed ADTs are meant to force this, but some languages only warn, not error, on non-exhaustive matchesTreat exhaustiveness warnings as errors (`-Wincomplete-patterns -Werror` and equivalents)
Overlapping/unreachable patterns shadowing later onesPatterns are tried top-to-bottom; an earlier wildcard or over-general pattern silently absorbs later casesEnable redundancy checking; order specific patterns before general ones
Confusing structural equality with reference equality on ADT valuesConstructors look like ordinary objects in some runtimes, tempting pointer comparisonRely on the language's derived structural equality for ADTs, not identity comparison
Infinite unfolding of recursive ADTs in eager languagesRecursive constructors (e.g. `Cons(x, xs)`) can be built lazily or strictly; eager evaluation of an unbounded generator loops foreverUse explicit laziness or bound recursion depth; understand the evaluation strategy of Recursion and Higher-Order Functions before relying on infinite structures

Pioneers & history

The theoretical roots of sum and product types trace to intuitionistic logic and the Curry–Howard correspondence, where sum types correspond to logical disjunction and product types to conjunction; pattern matching corresponds to disjunction elimination (case analysis) in natural deduction. The concrete language mechanism — algebraic data types with `data`/`type` declarations and exhaustive pattern matching as a first-class syntactic construct — was crystallized in the ML family of languages in the 1970s and carried forward through Miranda and Haskell, later reappearing with modern syntax in OCaml, F#, Scala, Swift, Rust, and TypeScript.

Origins · 1 minRobin Milner and how this was discovered
RM
Robin Milner
1934–2010 · Designer of ML and pioneer of type inference and process calculi

Robin Milner led the team that designed ML (Meta Language) at Edinburgh in the 1970s, originally as the tactic language for the LCF theorem prover. ML introduced `datatype` declarations with constructors and `case`/pattern-matching expressions essentially in the form still used today, alongside the Hindley–Milner type inference algorithm that lets such richly typed programs be checked without full type annotations.

History
1958
— John McCarthy's Lisp popularizes tagged S-expressions but without static tag checking.
1973
— Milner and colleagues design ML's `datatype` and pattern-matching constructs for LCF.
1977
— Robin Milner formalizes Hindley–Milner type inference, enabling ADTs without heavy annotation.
1985
— Miranda popularizes purely functional pattern matching with guards and exhaustiveness checks.
1990
— Haskell 1.0 standardizes algebraic data types, deriving, and exhaustive `case` as core language features.

Where this matters

Compilers themselves are a primary consumer: abstract syntax trees are almost universally represented as recursive ADTs (an `Expr` or `Stmt` sum type per node kind), and every compiler pass — parsing, type checking, optimization, code generation — is written as pattern matches over that tree, making ADTs the backbone of how programming languages implement other programming languages. Rust's `Option` and `Result` are ADTs that replace null pointers and unchecked exceptions with compiler-checked alternatives, directly preventing null-dereference bugs at the type level; Swift's `enum` with associated values plays the identical role in iOS/macOS application code. Functional-first industrial systems — financial modeling in OCaml and F#, blockchain and compiler tooling in Haskell, telecom systems in Erlang's pattern-matching-based clause dispatch — rely on ADTs to make protocol states, message formats, and parser outputs statically checkable. TypeScript's discriminated unions bring the same discipline to large JavaScript codebases, letting editors and compilers flag missing cases in UI state machines before deployment. The pattern also underlies parser combinator libraries, JSON/protobuf schema modeling, and the `Maybe`/`Either` types that motivate the treatment of Monads: An Introduction.

Ask the tutor: How does pattern-match compilation choose the order in which to test nested sub-patterns to minimize the number of runtime checks?

Are ADTs the same thing as "enums" in languages like C or Java?

Not quite. A C `enum` is a set of named integer constants with no attached data — it is a sum type where every constructor is nullary. Algebraic data types generalize this by letting each constructor carry its own tuple of typed arguments (a product), so a single ADT can express, in one declaration, a type like `Shape = Circle(Float) | Rectangle(Float, Float)`. Java's newer "sealed" classes with records, Swift's `enum` with associated values, and Rust's `enum` are closer analogues, since they combine the tagging behavior of a C `enum` with per-variant payloads and compiler-checked exhaustive `match`/`switch` support.

What exactly does "exhaustive" mean, and does a wildcard pattern always make a match exhaustive?

A match is exhaustive if every possible value of the scrutinee's type is matched by at least one pattern. A trailing wildcard pattern (`_ -> ...` or `default: ...`) trivially guarantees exhaustiveness because it matches anything left over, but this defeats the main safety benefit: if a new constructor is added later, the wildcard silently absorbs it instead of forcing the programmer to handle it explicitly. Idiomatic style in ADT-aware languages therefore favors listing every constructor explicitly and reserving wildcards for genuinely "don't care" situations, such as matching literal values within an otherwise unbounded domain like integers.

How does pattern matching interact with parametric polymorphism?

A polymorphic ADT such as `data List = Nil | Cons(A, List)` is exhaustiveness-checked purely on its constructor shape, independent of the type parameter `A`. Matching `Cons(x, xs)` binds `x : A` and `xs : List` for whatever `A` was instantiated at the call site, exactly mirroring how Parametric Polymorphism lets a single function definition work uniformly over all type instantiations. The exhaustiveness checker never needs to inspect `A` itself, since the two constructors `Nil` and `Cons` fully determine the possible shapes regardless of what `A` is.

Why is pattern matching described as the "dual" of constructors?

Constructors are introduction rules: they build a value of the ADT from its arguments, one constructor per rule. Pattern matching is the corresponding elimination rule: it consumes a value of the ADT by case-splitting on which constructor built it and recovering the original arguments. This introduction/elimination pairing is the same pattern found throughout typed lambda calculi — for instance, function abstraction (introduction) is dual to function application (elimination) — and it is what lets a type theorist prove, once and for all, that pattern matching over a correctly declared ADT can never "get stuck" on a value it doesn't recognize.

Do ADTs and pattern matching add runtime overhead compared to raw tagged unions in C?

No meaningful overhead beyond what a hand-written tagged union already pays. The runtime representation is the same — a tag plus a payload — and the compiled dispatch is the same integer comparison or jump table a C programmer would write by hand with a `switch` on a manually maintained tag field. The benefit is entirely at compile time: the language automatically maintains the tag correctly, forbids constructing a value with a mismatched tag/payload pair, and checks that every tag is handled somewhere, none of which a raw C `union` enforces.

What happens if two patterns in a match overlap?

Overlapping patterns are legal as long as they are ordered correctly, since matching proceeds top to bottom and the first pattern that matches wins — this is how guard-based refinements and progressively more specific patterns are meant to work (e.g., a specific literal pattern before a catch-all variable pattern). What compilers flag as an error or warning is redundancy: a later pattern that can never fire because every value it could match was already consumed by an earlier pattern, detected by the same uncovered-set bookkeeping used for exhaustiveness, run in reverse.

How do ADTs relate to what comes next, like recursion and monads?

Recursive ADTs such as lists and trees are the standard data structures over which structurally recursive functions are defined, which is the organizing idea behind Recursion and Higher-Order Functions — a function recurses exactly along the recursive constructor arguments of its input type. Looking further ahead, types like `Option = None | Some(A)` and `Result = Ok(A) | Err(E)` are themselves simple ADTs, and the operations that sequence computations over them (mapping and "binding" through the `Some`/`Ok` case while short-circuiting on `None`/`Err`) are the motivating first examples used to introduce Monads: An Introduction.

Before this

Where this leads