Denotational Design
Guide a project from "what should this mean?" to a proven, tested, governed
implementation, using Conal Elliott's denotational design discipline: give
every key type a precise mathematical meaning, specify every operation by
the single requirement that the meaning function is a homomorphism, solve
those equations for the implementation rather than verifying guesses, refine
representations without ever moving the meaning, and accept realizations on
empirical evidence whose limits are declared.
The method in one line: before asking how it runs, ask what it means;
then require that the meaning be preserved; let the implementation be
whatever the equations force it to be.
The method at teaching scale, in Elliott's own words — "this plan holds
for library design in general": (1) define the abstract interface and its
denotation — "this is the most important thing; this is what it all
means"; (2) enumerate candidate representations — "radically different ways
to implement the interface, every one of which respects the denotation";
(3) pick one; (4) calculate the implementation from the specification.
The ten phases below are that four-step recipe expanded for a project with
a proof assistant, multiple levels, and many hands. On a small design, the
four steps are the whole method.
The simplicity criterion is a test, not a taste. A meaning is elegant
when it can be stated very concisely in mathematics that already exists
for other reasons (Gell-Mann's definition, which Elliott adopts —
"already learned" means collectively, so "now I'm finally motivated to
learn it" passes and bespoke machinery invented for this one design fails).
The pre-existence clause is what makes the claim falsifiable: otherwise one
can define a complicated object, name it, and point at the name. And when
someone calls a candidate "simpler", check whether they mean familiar —
"often when people say simple, they really mean familiar."
When to use, and when not
Design a vocabulary, not a language. Elliott's scope is narrower than
"anything with a semantics": "I don't design languages… what I do is I
design programming interfaces and implementations." Landin's split governs:
a domain-independent host language plus an embedded domain vocabulary —
reinvent only the second. The positive admission test is infinite
expressiveness: a vocabulary whose values compose into unboundedly many
more values — a library, DSL-as-library, compiler, runtime, or protocol.
Applications get amnesty: "applications are quite rigid… you don't expect
to be systematic." When only one type in a design carries composition,
that is the one type to denote. When a capability genuinely cannot be a
plain library (automatic differentiation is non-computable at the host's
function semantics) or the target is exotic (hardware, mesh processors),
the remedy is compiling the host language (Compiling to Categories), never
a graph-building API — "the funkiness shouldn't be visible in my API;
that's an abstraction leak."
Do NOT apply the full method to I/O glue, config parsing, log formatting,
migration scripts, test scaffolding, or build tooling. The principled
reason: those fragments are not denotative — the meaning of an
expression does not depend only on the meanings of its components (Landin,
1966) — so there is no compositional meaning for ⟦·⟧ to be a homomorphism
over. The antidote is the usual one: make the effects into values,
interpret them with one driver at the edge, and denote the values. Own the
carve-out honestly: it is this skill's effort budget, not method doctrine —
Elliott rejects paradigm lines drawn by scale or layer ("functional core
and an imperative shell… stop and don't believe it") — so never cite it as
license for a denoted core with an undenoted shell. The proportionate
minimum for borderline cases: name the mathematical object and write one
line of ⟦·⟧, then stop.
Do not reach for the prover before the meaning. Formalizing a design
that was conceived operationally — sequential, stateful, effect-ordered —
produces an artificially difficult proof that teaches nothing: success
shows only that a hard problem was solved. For such a codebase the
retrofit opening (write down the denotation it implicitly has, defects
included) is the work; proof comes after the meaning moves, not instead of
it.
The process
Run the phases below as a dialog. Each phase has questions to put to
the user, an artifact to produce, and an exit test. Do not advance a phase
without its artifact. The full interview script, with per-phase questions
and exit criteria, is in references/dialog-protocol.md — read it first
and follow it. The phases:
- The principal mathematical object — its shape and definition, found
by subtraction, confirmed by the identity test.
- The fundamental operations on that object — forced by structure or
documented need, named by standard algebraic classes.
- The fundamental theorems on that object and those operations —
equations with downstream force, plus an explicit non-theorems list.
- Derived operations and theorems — exploration; recognize before
inventing, delete bespoke names that standard classes supply.
- Candidate representation objects — first a fan of radically
different candidates, then a pick; the tower from the mathematical
object down to the final realization.
- Denotations between representations — each level's meaning function
into the level above, composing into the mathematical object's type.
- Typed operations per representation — solved from the homomorphism
equations, not written and checked.
- Commuting theorem statements and proofs — the squares that establish
each representation's semantics, machine-checked.
- Empirical realization testing — quality, efficiency, performance,
runtime behavior, accepted on pre-committed criteria.
- Cross-language realization fidelity — bisimulation of (e.g.) a Rust
implementation against its proof-assistant oracle.
Phases 1–4 fix the meaning (kernel classes K1–K3 plus derived layer);
phases 5–8 fix the representations and their correctness (K4–K6);
phases 9–10 fix the realizations, which are deliberately outside the
proof kernel and governed empirically.
Provenance. Phases 1–8 and the method references distill Elliott's
corpus and talks. Phases 9–10 and references/governance.md are a
program-scale extension from a different lineage: in Elliott's own talks
the proofs are hand calculations in an appendix and efficiency claims are
labelled impressions ("I don't have measurements to back that up"). On a
small project the derivation itself is the reviewable proof artifact, and
an unmeasured performance claim should be labelled as one rather than
dressed as a gate.
How to work each phase
- Method (phases 1–8): the distilled Elliott discipline — the
subtraction opening, the denotation repertoires, the shape→classes rule,
the solving loop, the failure-diagnosis table (a morphism equation that
will not close is the method's most valuable output), and the completion
tests — is in
references/denotational-method.md. Consult it before and
during every design conversation; the diagnostic table is the part to
have open when an equation resists.
- Worked example:
references/worked-example-images.md reconstructs
Elliott's image-library seminar end to end — candidate board, semantic
equations, the generalization cascade, the class sweep, and the four
recorded warts. Read it before running the dialog for the first time,
and mine it for phase-appropriate exhibits during any engagement.
- Objections: every engagement will surface some of the standard
objections (discreteness, full abstraction, "too slow for industry",
"nobody can learn Agda", …). The answers Elliott actually gives — with
his own grading of which objections are substantive — are in
references/objections.md. Do not improvise these.
- Prover (phases 6–8): the design defaults to Lean 4, with Rocq and
Agda as supported alternatives. Selection criteria, per-prover idioms for
the method's artifacts, and extraction paths are in
references/provers.md. Ask the user's preference in phase 1; do not
re-litigate it later without cause.
- Realizations (phases 9–10): acceptance cards committed before the
work, sampled-square evidence with pinned comparison tuples, evidence
ceilings, and the bisimulation strategies for a foreign-language
realization against its oracle are in
references/realization-and-bisimulation.md.
- Governance (all phases, scaled to the project): the knowledge-kernel
partition, the spec-tower file scaffold, law IDs and the obligation
ledger, judged decision records, and the scale-down table are in
references/governance.md.
The worksheet
Produce the design as a living document seeded from
assets/design-worksheet.md — copy it into the user's project (e.g.
doc/design/denotational-design.md or split per level as it grows) and
fill it in phase by phase. The worksheet's sections mirror the phases, so
an empty section is a visible unmet obligation rather than an absence
nobody notices.
Ground rules (hold these in every phase)
- Meaning constrains implementation; implementation never constrains
meaning. Specification and realization are pulled in opposite
directions — the spec toward precise simplicity, the realization toward
the machine actually in front of you — and each should be pushed to its
extreme: "if you conflate those two things you cannot win, or one can
only win at the other's loss." The theorem is not a tax on the fast
path; it is the permission slip for it. One honest exception, taken
openly: where the semantically right meaning is not computable, Elliott
has substituted the computable neighbour and recorded the wart, naming
the declined alternative ("I'm not entirely proud of it"). Never take
that move silently, and never for a whole object — only an operation's
convenience is sacrificed, on the record.
- Solve, do not verify. Every homomorphism equation is an algebra
problem with exactly one unknown — the representation's operation — and
every solution is correct; implementations are read off solved forms,
and efficiency work is the choice among solutions.
- The specification may be unrunnable. Prefer the non-computable
meaning that is clear over the executable one that is compromised — at
no operational cost, because "we only compute with representations; we
only think with meanings."
- Compose first, approximate last. Approximation is legitimate and
usually unavoidable; approximating before composing is not, because
errors that are individually small compound past usefulness under
composition. Resolution, sample rates, frame boundaries, step sizes, and
fixed precision live at exactly one level — the bottom — applied once
after everything has been combined. "Compositionality depends on
perfection," and it is the method's entire leverage.
- Adequacy admits, efficiency vetoes, simplicity decides. A model that
cannot answer the domain's questions is disqualified whatever its
beauty; so is one whose realizations are unaffordably slow ("elegance
isn't worth a 50× hit in efficiency — I completely agree"). Simplicity
is the tiebreaker among candidates that already pass both, never an
admission criterion.
- Laws come "already paid for" under exactly three provisos — the
specification is in homomorphic form, equality is semantic
(
a ≡ b ⟺ ⟦a⟧ ≡ ⟦b⟧), and the type stays abstract. A type that does not
define equality through its meaning has no meaning function, whatever
the candidate is called; a leaked constructor voids the transfer.
- A failed morphism is a finding, not a defeat. Diagnose it against the
failure table and repair the model, choosing the repair that improves
overall simplicity.
- The derived vocabulary is a compilation target, not a user
interface. The point-free/categorical form is where reinterpretation
becomes possible, not where people write. Keep the friendly surface
(lambdas, ordinary functions) and generate the target form mechanically.
A generated graph, tape, or matrix is fine and often the payoff;
requiring the user to construct one when the host language can
express the thing directly is the defect.
- The prover is an amplifier, not a constituent. The minimum viable
home for a specification is a comment and the documentation, optionally
a property test; the minimum viable proof is the derivation read
backwards, recorded beside the definition. Do not gate phase 1 on
tooling; reach for Lean/Rocq/Agda when the design has many levels, many
hands, or laws whose proofs are no longer one-liners.
- Evidence ceilings are declared, not inferred. Every artifact states
what its passing does not prove; a diagnostic never upgrades to semantic
evidence by citation.
- Gates only tighten. Empirical criteria are committed before the work
and may only tighten afterward; a gate is never weakened to land a step.
- Honest costs, stated up front: the method demands up-front clarity
and some unlearning of presentation-oriented habits; in a host language
that cannot state correctness, a human becomes the checker, repeats
the same verdict indefinitely, and burns out (the documented reason
denotational design outgrew Haskell-plus-discipline); the method admits
no sanctioned ugly fallback, so an equation that will not close is a
redesign whose schedule risk the practitioner absorbs; and there is an
institutional cost — from outside, the method reads as a refusal to
compromise ("Conal needs to learn to compromise better" was a recurring
performance review). What is not a cost: the meaning/representation
split is what makes aggressive optimization safe — without proof,
optimization stalls where the author's grip on correctness gets too
shaky to continue ("proof is the necessary ally of efficiency"), and
Elliott's maximally permissive image model cost "really none that I
could notice" in a real-time interactive system. Some types may resist
the discipline entirely — publish the exception as an open problem
rather than pretending the morphism holds.
1---2name: denotational-design3description: This skill should be used when applying denotational design (Conal Elliott's method) to a project — designing a library, compiler, runtime, or system around a precise mathematical meaning. It guides a structured dialog to choose the principal mathematical object, its operations and theorems, a tower of representation objects with denotations between them, per-representation operation definitions solved from homomorphism (commuting) equations, machine-checked proofs in Lean/Rocq/Agda, empirical realization testing, and cross-language bisimulation (e.g., a Rust realization against its proof-assistant oracle). Triggers include "denotational design", "meaning function", "what does this type mean", "semantic domain", "type class morphism", "commuting theorems", "representation tower", "design this denotationally", or a request to give an existing codebase a formal semantic basis.4---56# Denotational Design78Guide a project from "what should this mean?" to a proven, tested, governed9implementation, using Conal Elliott's denotational design discipline: give10every key type a precise mathematical meaning, specify every operation by11the single requirement that the meaning function is a homomorphism, *solve*12those equations for the implementation rather than verifying guesses, refine13representations without ever moving the meaning, and accept realizations on14empirical evidence whose limits are declared.1516The method in one line: **before asking how it runs, ask what it means;17then require that the meaning be preserved; let the implementation be18whatever the equations force it to be.**1920The method at teaching scale, in Elliott's own words — "this plan holds21for library design in general": (1) define the abstract interface *and its22denotation* — "this is the most important thing; this is what it all23means"; (2) enumerate candidate representations — "radically different ways24to implement the interface, every one of which respects the denotation";25(3) pick one; (4) **calculate** the implementation from the specification.26The ten phases below are that four-step recipe expanded for a project with27a proof assistant, multiple levels, and many hands. On a small design, the28four steps are the whole method.2930**The simplicity criterion is a test, not a taste.** A meaning is elegant31when it can be stated very concisely in mathematics that already exists32*for other reasons* (Gell-Mann's definition, which Elliott adopts —33"already learned" means collectively, so "now I'm finally motivated to34learn it" passes and bespoke machinery invented for this one design fails).35The pre-existence clause is what makes the claim falsifiable: otherwise one36can define a complicated object, name it, and point at the name. And when37someone calls a candidate "simpler", check whether they mean *familiar* —38"often when people say simple, they really mean familiar."3940## When to use, and when not4142**Design a vocabulary, not a language.** Elliott's scope is narrower than43"anything with a semantics": "I don't design languages… what I do is I44design programming interfaces and implementations." Landin's split governs:45a domain-independent host language plus an embedded domain vocabulary —46reinvent only the second. The positive admission test is *infinite47expressiveness*: a vocabulary whose values compose into unboundedly many48more values — a library, DSL-as-library, compiler, runtime, or protocol.49Applications get amnesty: "applications are quite rigid… you don't expect50to be systematic." When only one type in a design carries composition,51that is the one type to denote. When a capability genuinely cannot be a52plain library (automatic differentiation is non-computable at the host's53function semantics) or the target is exotic (hardware, mesh processors),54the remedy is compiling the host language (Compiling to Categories), never55a graph-building API — "the funkiness shouldn't be visible in my API;56that's an abstraction leak."5758Do NOT apply the full method to I/O glue, config parsing, log formatting,59migration scripts, test scaffolding, or build tooling. The principled60reason: those fragments are not *denotative* — the meaning of an61expression does not depend only on the meanings of its components (Landin,621966) — so there is no compositional meaning for `⟦·⟧` to be a homomorphism63over. The antidote is the usual one: make the effects into values,64interpret them with one driver at the edge, and denote the values. Own the65carve-out honestly: it is this skill's effort budget, not method doctrine —66Elliott rejects paradigm lines drawn by scale or layer ("functional core67and an imperative shell… stop and don't believe it") — so never cite it as68license for a denoted core with an undenoted shell. The proportionate69minimum for borderline cases: name the mathematical object and write one70line of `⟦·⟧`, then stop.7172**Do not reach for the prover before the meaning.** Formalizing a design73that was conceived operationally — sequential, stateful, effect-ordered —74produces an artificially difficult proof that teaches nothing: success75shows only that a hard problem was solved. For such a codebase the76retrofit opening (write down the denotation it implicitly has, defects77included) is the work; proof comes after the meaning moves, not instead of78it.7980## The process8182Run the phases below **as a dialog**. Each phase has questions to put to83the user, an artifact to produce, and an exit test. Do not advance a phase84without its artifact. The full interview script, with per-phase questions85and exit criteria, is in `references/dialog-protocol.md` — read it first86and follow it. The phases:87881. **The principal mathematical object** — its shape and definition, found89 by subtraction, confirmed by the identity test.902. **The fundamental operations** on that object — forced by structure or91 documented need, named by standard algebraic classes.923. **The fundamental theorems** on that object and those operations —93 equations with downstream force, plus an explicit non-theorems list.944. **Derived operations and theorems** — exploration; recognize before95 inventing, delete bespoke names that standard classes supply.965. **Candidate representation objects** — first a fan of radically97 different candidates, then a pick; the tower from the mathematical98 object down to the final realization.996. **Denotations between representations** — each level's meaning function100 into the level above, composing into the mathematical object's type.1017. **Typed operations per representation** — solved from the homomorphism102 equations, not written and checked.1038. **Commuting theorem statements and proofs** — the squares that establish104 each representation's semantics, machine-checked.1059. **Empirical realization testing** — quality, efficiency, performance,106 runtime behavior, accepted on pre-committed criteria.10710. **Cross-language realization fidelity** — bisimulation of (e.g.) a Rust108 implementation against its proof-assistant oracle.109110Phases 1–4 fix the *meaning* (kernel classes K1–K3 plus derived layer);111phases 5–8 fix the *representations and their correctness* (K4–K6);112phases 9–10 fix the *realizations*, which are deliberately outside the113proof kernel and governed empirically.114115**Provenance.** Phases 1–8 and the method references distill Elliott's116corpus and talks. Phases 9–10 and `references/governance.md` are a117program-scale extension from a different lineage: in Elliott's own talks118the proofs are hand calculations in an appendix and efficiency claims are119labelled impressions ("I don't have measurements to back that up"). On a120small project the derivation itself is the reviewable proof artifact, and121an unmeasured performance claim should be labelled as one rather than122dressed as a gate.123124## How to work each phase125126- **Method** (phases 1–8): the distilled Elliott discipline — the127 subtraction opening, the denotation repertoires, the shape→classes rule,128 the solving loop, the failure-diagnosis table (a morphism equation that129 will not close is the method's most valuable output), and the completion130 tests — is in `references/denotational-method.md`. Consult it before and131 during every design conversation; the diagnostic table is the part to132 have open when an equation resists.133- **Worked example**: `references/worked-example-images.md` reconstructs134 Elliott's image-library seminar end to end — candidate board, semantic135 equations, the generalization cascade, the class sweep, and the four136 recorded warts. Read it before running the dialog for the first time,137 and mine it for phase-appropriate exhibits during any engagement.138- **Objections**: every engagement will surface some of the standard139 objections (discreteness, full abstraction, "too slow for industry",140 "nobody can learn Agda", …). The answers Elliott actually gives — with141 his own grading of which objections are substantive — are in142 `references/objections.md`. Do not improvise these.143- **Prover** (phases 6–8): the design defaults to Lean 4, with Rocq and144 Agda as supported alternatives. Selection criteria, per-prover idioms for145 the method's artifacts, and extraction paths are in146 `references/provers.md`. Ask the user's preference in phase 1; do not147 re-litigate it later without cause.148- **Realizations** (phases 9–10): acceptance cards committed before the149 work, sampled-square evidence with pinned comparison tuples, evidence150 ceilings, and the bisimulation strategies for a foreign-language151 realization against its oracle are in152 `references/realization-and-bisimulation.md`.153- **Governance** (all phases, scaled to the project): the knowledge-kernel154 partition, the spec-tower file scaffold, law IDs and the obligation155 ledger, judged decision records, and the scale-down table are in156 `references/governance.md`.157158## The worksheet159160Produce the design as a living document seeded from161`assets/design-worksheet.md` — copy it into the user's project (e.g.162`doc/design/denotational-design.md` or split per level as it grows) and163fill it in phase by phase. The worksheet's sections mirror the phases, so164an empty section is a visible unmet obligation rather than an absence165nobody notices.166167## Ground rules (hold these in every phase)168169- **Meaning constrains implementation; implementation never constrains170 meaning.** Specification and realization are pulled in *opposite*171 directions — the spec toward precise simplicity, the realization toward172 the machine actually in front of you — and each should be pushed to its173 extreme: "if you conflate those two things you cannot win, or one can174 only win at the other's loss." The theorem is not a tax on the fast175 path; it is the permission slip for it. One honest exception, taken176 openly: where the semantically right meaning is not computable, Elliott177 has substituted the computable neighbour and *recorded the wart*, naming178 the declined alternative ("I'm not entirely proud of it"). Never take179 that move silently, and never for a whole object — only an operation's180 convenience is sacrificed, on the record.181- **Solve, do not verify.** Every homomorphism equation is an algebra182 problem with exactly one unknown — the representation's operation — and183 every solution is correct; implementations are read off solved forms,184 and efficiency work is the choice *among* solutions.185- **The specification may be unrunnable.** Prefer the non-computable186 meaning that is clear over the executable one that is compromised — at187 no operational cost, because "we only compute with representations; we188 only think with meanings."189- **Compose first, approximate last.** Approximation is legitimate and190 usually unavoidable; approximating *before* composing is not, because191 errors that are individually small compound past usefulness under192 composition. Resolution, sample rates, frame boundaries, step sizes, and193 fixed precision live at exactly one level — the bottom — applied once194 after everything has been combined. "Compositionality depends on195 perfection," and it is the method's entire leverage.196- **Adequacy admits, efficiency vetoes, simplicity decides.** A model that197 cannot answer the domain's questions is disqualified whatever its198 beauty; so is one whose realizations are unaffordably slow ("elegance199 isn't worth a 50× hit in efficiency — I completely agree"). Simplicity200 is the tiebreaker among candidates that already pass both, never an201 admission criterion.202- **Laws come "already paid for" under exactly three provisos** — the203 specification is in homomorphic form, equality is semantic204 (`a ≡ b ⟺ ⟦a⟧ ≡ ⟦b⟧`), and the type stays abstract. A type that does not205 define equality through its meaning has no meaning function, whatever206 the candidate is called; a leaked constructor voids the transfer.207- **A failed morphism is a finding, not a defeat.** Diagnose it against the208 failure table and repair the model, choosing the repair that improves209 overall simplicity.210- **The derived vocabulary is a compilation target, not a user211 interface.** The point-free/categorical form is where reinterpretation212 becomes possible, not where people write. Keep the friendly surface213 (lambdas, ordinary functions) and generate the target form mechanically.214 A *generated* graph, tape, or matrix is fine and often the payoff;215 requiring the **user** to construct one when the host language can216 express the thing directly is the defect.217- **The prover is an amplifier, not a constituent.** The minimum viable218 home for a specification is a comment and the documentation, optionally219 a property test; the minimum viable proof is the derivation read220 backwards, recorded beside the definition. Do not gate phase 1 on221 tooling; reach for Lean/Rocq/Agda when the design has many levels, many222 hands, or laws whose proofs are no longer one-liners.223- **Evidence ceilings are declared, not inferred.** Every artifact states224 what its passing does not prove; a diagnostic never upgrades to semantic225 evidence by citation.226- **Gates only tighten.** Empirical criteria are committed before the work227 and may only tighten afterward; a gate is never weakened to land a step.228- **Honest costs, stated up front:** the method demands up-front clarity229 and some unlearning of presentation-oriented habits; in a host language230 that cannot *state* correctness, a human becomes the checker, repeats231 the same verdict indefinitely, and burns out (the documented reason232 denotational design outgrew Haskell-plus-discipline); the method admits233 no sanctioned ugly fallback, so an equation that will not close is a234 redesign whose schedule risk the practitioner absorbs; and there is an235 institutional cost — from outside, the method reads as a refusal to236 compromise ("Conal needs to learn to compromise better" was a recurring237 performance review). What is *not* a cost: the meaning/representation238 split is what makes aggressive optimization safe — without proof,239 optimization stalls where the author's grip on correctness gets too240 shaky to continue ("proof is the necessary ally of efficiency"), and241 Elliott's maximally permissive image model cost "really none that I242 could notice" in a real-time interactive system. Some types may resist243 the discipline entirely — publish the exception as an open problem244 rather than pretending the morphism holds.