ch:categories: ch:categories
Problem, invariant, and acceptance test. Build the category
Representation. Terms are a datatype with named binders,
data Tm : Type = Var String | Lam String Ty Tm | App Tm Tm
| BTrue | BFalse | If Tm Tm Tm
and an arrow is a triple of its source, its target, and its components,
data Subst : Type = Subst Ctx Ctx (List Tm)
The raw constructor is private to the companion module. Its makeSubst boundary and checked composition protect uses of this representation: the constructor verifies that the two contexts have distinct declared names and that every component has its target declaration’s type; composition first validates both input arrows, verifies that the target of its first arrow is the source of its second, and returns either a checked arrow or an explicit rejection. The named corpus fixtures use the private constructor, but each is checked before it participates in a passing case. Carrying the target inside the arrow is what makes the action definable without a separate argument: the component list is zipped with the target’s variable names to form the replacement map. The alternative is de Bruijn indices, which remove renaming at binders entirely and make
The first version that runs. Write the type checker first, since arrows are defined by typing. It has one clause per rule of chapter 2:
typeOf : Ctx -> Tm -> Typed
let typeOf ctx term decreases structural term =
match term
case Var x -> lookupDecl x ctx
case Lam x ty body ->
match typeOf (Decl x ty :: ctx) body
case HasType bodyTy -> HasType (TyArrow ty bodyTy)
case Untyped why -> Untyped why
case App f a -> ... -- require f : dom -> cod and a : dom
case If guard yes no ->
... -- require guard : Bool and equal branch types
Then define wellTyped on an arrow by checking the two contexts and each component against the corresponding declared type of the target. makeSubst uses this predicate rather than leaving it to callers. It validates
The cases of the metatheory. The action is simultaneous substitution, and its cases are the cases of the proof of lemma 141.5. The variable case looks up the replacement map, leaving unbound names unchanged; application and the three subterms of a conditional distribute. The binder case is the one the printed proof singled out:
case Lam x ty body ->
let inner = dropBind x binds
let avoid = union (rangeVars inner)
(union (domainVars inner) (freeVars body))
let y = fresh (1 + nameCount avoid) x avoid
if y == x then Lam x ty (act body inner)
else Lam y ty (act body (Bind x (Var y) :: inner))
The avoid-set is finite. If it has 1 + nameCount avoid is both a termination measure and a sufficient search bound. The acceptance test uses the seventeen candidates that would have exhausted the former fixed bound of dropBind) because the abstraction clause substitutes only for the free occurrences. Second, the avoid-set contains the free variables of the range, which is the freshness condition of convention 2.3; it also contains the domain and the body’s own free variables, so that renaming cannot create a new capture. Third, when renaming is needed, the body is substituted with makeSubst; a mismatch returns Noncomposable without constructing an arrow. The identity is the list of the context’s variables. The Yoneda component of a term
The failure the reader will hit. Delete the freshening: replace let y = fresh (1 + nameCount avoid) x avoid by let y = x. The program still typechecks, the first composite still prints
Discharging the acceptance test. Run the corpus with the command recorded in appendix E. The eleven case lines, followed by the summary, are
PASS first composite: (\w:Bool. f x)
PASS associativity instance: ((\w:Bool. f x) false) both ways
PASS capture instance: \x':Bool. x keeps x free
PASS identity laws: id.sigma = sigma = sigma.id
PASS seminar binder composite: action law holds
PASS Yoneda component: f true
PASS Yoneda recovery: x true
PASS named/nameless equivalence: (f, x) inverts (x1, x2)
PASS conditional action: if true then f x else false
PASS rejected noncomposable arrows
PASS fresh-name boundary avoids 17 forbidden names
All 11 Chapter 141 corpus cases passed.
Check each against the chapter: the first composite is (141.1), with the bound name
What the program does not prove. The run shows that the substitution algebra behaves as proposition 141.7 says on the named inputs, rejects one ill-matched composition boundary, exercises every term constructor, checks one Yoneda bijection on one term, and checks that one renaming is invertible. It does not prove lemma 141.5, which quantifies over all terms and all substitutions, and it does not prove theorem 141.52; the first is proved in the chapter by induction and the second by calculation.