6. The Small Print: Parametricity
Quantifying over v is meant to stop a term from depending on which v it
is given. That guarantee is parametricity: a polymorphic
definition behaves the same way at every type.The word is
Christopher Strachey's (1967); the relational formulation used below is John
Reynolds' (1983). Philip Wadler's paper "Theorems for free!" (1989) made it
popular with programmers. For many languages it is a theorem about the
language, proved from outside. Lean's logic does not contain it, and
classical reasoning can branch on which type a definition was handed. The
interpretations of the "same" term can then disagree.Lean's logic
includes the law of the excluded middle, so if v = Unit then … else … is
allowed even though no program could decide it. Such definitions cannot be
compiled, and are marked noncomputable.
namespace PHOAS
open Term'
open Classical in
noncomputable def chameleon : Term := fun v =>
if v = Unit then two v else I v
This is an exotic term of a new kind: it does not inspect its
argument, it inspects the type. Counting sees two; printing sees I.
Lean needs a proof even that String ≠ Unit: if the two were equal, ▸
would carry "all Unit values are equal" over to String, giving
"a" = "b".simp rewrites the goal with the listed definitions
and lemmas, plus a standard set, until it closes.
theorem string_ne_unit : String ≠ Unit := ⊢ String ≠ Unit
h:String = Unit⊢ False
h:String = Unitthis:∀ (a b : String), a = b⊢ False
exact absurd (this "a" "b") (h:String = Unitthis:∀ (a b : String), a = b⊢ ¬"a" = "b" All goals completed! 🐙)
example : chameleon.count = 3 := ⊢ chameleon.count = 3
All goals completed! 🐙
example : chameleon.toNamed = .lam "x0" (.var "x0") := ⊢ chameleon.toNamed = Named.Term.lam "x0" (Named.Term.var "x0")
⊢ toString "x" ++ Nat.repr 0 = "x0"
All goals completed! 🐙
6.1. Well-Formedness
The fix is to state the missing uniformity for each term, as a property we
can prove, instead of expecting the type to guarantee it. Interpret the term
at two variable types at once, and require the two results to have the same
shape, with variables paired up by the binder that introduced them. This is
Reynolds' relational parametricity in miniature: a relation between two
instantiations that the term must respect. Γ lists the pairs of variables
bound so far.An inductive … → Prop defines a relation by rules:
each constructor is one way to establish it. Names such as Γ and b₁ that
are used without being declared become implicit arguments.
inductive Term'.Equiv {v₁ v₂ : Type} :
List (v₁ × v₂) → Term' v₁ → Term' v₂ → Prop
| var : (x, y) ∈ Γ → Equiv Γ (var x) (var y)
| lam : (∀ x y, Equiv ((x, y) :: Γ) (b₁ x) (b₂ y)) →
Equiv Γ (lam b₁) (lam b₂)
| app : Equiv Γ f₁ f₂ → Equiv Γ a₁ a₂ →
Equiv Γ (app f₁ a₁) (app f₂ a₂)
def Term.Wf (e : Term) : Prop :=
∀ v₁ v₂, Term'.Equiv [] (e v₁) (e v₂)
Honest terms are well-formed by a proof that mirrors their shape:
theorem two_wf : two.Wf := fun _ _ =>
.lam fun _ _ => .lam fun _ _ =>
.app (.var (x✝⁵:Typex✝⁴:Typex✝³:x✝⁵x✝²:x✝⁴x✝¹:x✝⁵x✝:x✝⁴⊢ (x✝³, x✝²) ∈ [(x✝¹, x✝), (x✝³, x✝²)] All goals completed! 🐙))
(.app (.var (x✝⁵:Typex✝⁴:Typex✝³:x✝⁵x✝²:x✝⁴x✝¹:x✝⁵x✝:x✝⁴⊢ (x✝³, x✝²) ∈ [(x✝¹, x✝), (x✝³, x✝²)] All goals completed! 🐙)) (.var (x✝⁵:Typex✝⁴:Typex✝³:x✝⁵x✝²:x✝⁴x✝¹:x✝⁵x✝:x✝⁴⊢ (x✝¹, x✝) ∈ [(x✝¹, x✝), (x✝³, x✝²)] All goals completed! 🐙)))
The chameleon is not:
theorem chameleon_not_wf : ¬ chameleon.Wf := ⊢ ¬chameleon.Wf
h:chameleon.Wf⊢ False
h✝:chameleon.Wfh:Equiv [] (chameleon Unit) (chameleon String)⊢ False
h✝:chameleon.Wfh:Equiv [] (lam fun f => lam fun x => (var f).app ((var f).app (var x))) (lam fun x => var x)⊢ False
cases h with
h✝:chameleon.Wfh:∀ (x : Unit) (y : String),
Equiv [(x, y)] ((fun f => lam fun x => (var f).app ((var f).app (var x))) x) ((fun x => var x) y)⊢ False All goals completed! 🐙
end PHOAS
Theorems that relate two interpretations, such as "printing then parsing
gives back the term", need Wf as a hypothesis. Theorems that use a single
interpretation do not, and the next chapters live entirely in that case.