Unified Interpreters For Dependent Languages

Posted on October 30, 2015

I’ve spent the past couple months playing around with all sorts of ways to represent type theory in type theory. Doing so has led me down a lot of rabbit holes, and in this post I hope to explain some of the more fun ones.

Induction Recursion

Induction recursion has become one of the most powerful tools in my type theoretic utility belt. One classic example of induction recursion is defining a universe of types within another universe inductively (which we can then eliminate over). The construction is simple enough - we just define an inductive family of codes at the same time as the decoding function.

data U : Set_⟧ᵤ : U  Set

data U where
  π  σ    : (A : U)  (⟦ A ⟧ᵤ  U)  U -- Π, Σ
  n₀ n₁ n₂ : U -- ⊥,⊤

⟦ π a b ⟧ᵤ = (a↓ : ⟦ a ⟧ᵤ)  ⟦ b a↓ ⟧ᵤ
⟦ σ a b ⟧ᵤ = Σ ⟦ a ⟧ᵤ  a↓  ⟦ b a↓ ⟧ᵤ)
⟦ n₀ ⟧ᵤ = ⊥
⟦ n₁ ⟧ᵤ = ⊤
⟦ n₂ ⟧ᵤ = Bool

We can actually go a little farther, and define next universes for every universe in type theory.

data NextU (U' : Set) (_⟧ᵤ' : U'  Set) : Set_⟧ₙ :  {U' ⟦_⟧ᵤ'}  NextU U' ⟦_⟧ᵤ'  Set

data NextU U' ⟦_⟧ᵤ' where
  uu : NextU U' ⟦_⟧ᵤ'
  up : U'  NextU U' ⟦_⟧ᵤ'
  πu σu : (A : NextU U' ⟦_⟧ᵤ')  (⟦ A ⟧ₙ  NextU U' ⟦_⟧ᵤ')  nextU U' ⟦_⟧ᵤ' -- Π, Σ_⟧ₙ {U'} uu = U'
⟦_⟧ₙ {U'} {_⟧ᵤ'} (up u) = ⟦ u ⟧ᵤ'
⟦ πu a b ⟧ₙ = (a↓ : ⟦ a ⟧ₙ)  ⟦ b a↓ ⟧ₙ
⟦ σu a b ⟧ₙ = Σ ⟦ a ⟧ₙ  a↓  ⟦ b a↓ ⟧ₙ)

You can even define Mahlo universes in Set this way, but I haven’t actually found much use for that.

Representing languages

Using induction induction, it’s fairly easy to define a dependently typed language. We’ll use a typed variant on DeBrujin indices to represent variable binding.

data Con : Set
data Ty : Con  Set
data __ :  Γ  Ty Γ  Set
data __ (Γ : Con) : Ty Γ  Set

data Con where
  ε : Con
  _,_ :  Γ  Ty Γ  Con

data Ty where
  π :  {Γ} (A : Ty Γ)  Ty (Γ , A)  Ty Γ
  ‘⊤’ ‘⊥’ :  {Γ}  Ty Γ
  W :  {Γ A}  Ty Γ  Ty (Γ , A) -- weakening
  __ :  {Γ A} (B : Ty (Γ , A))  Γ ⊢ A  Ty Γ -- application at type level

data __ where
  top :  {Γ A}  (Γ , A) ∋ W A
  pop :  {Γ A B}  Γ ∋ A  (Γ , B) ∋ W A

data __ where
  var :  {Γ A}  Γ ∋ A  Γ ⊢ A
  lam :  {Γ} A (B : Ty (Γ , A))  (Γ , A) ⊢ B  Γ ⊢ π A B
  __ : A B}  Γ ⊢ π A B  (a : Γ ⊢ A)  Γ ⊢ B ⍟ a
  tt :  {Γ}  Γ ⊢ ‘⊤’ -- ‘⊤’ is terminal
  exf :  {Γ A}  Γ ⊢ ‘⊥’  Γ ⊢ A -- ‘⊥’ is initial

We could then define an interpreter for this langauge in three parts - one function to interpret contexts, one for types, and one for terms.

Con↓ : Con  Set -- Denotation of a context should be a list of typed holes to fill in an expression - think of this as the map we'll look up variables in
Ty↓ :  {Γ}  Ty Γ  Con↓ Γ  Set -- Denotation of a type should be, well, a type, but should only be accessible if we can fill in all the holes
Tm↓ :  {Γ T}  Γ ⊢ T  (Γ↓ : Con↓ Γ)  Ty↓ T Γ↓ -- Denotation of a term should be an inhabitant of its type, but again can only be created if we can give the variable lookup

Con↓ ε =-- empty context is trivial
Con↓ (Γ , T) = Σ[ Γ↓ ∈ Con↓ Γ ] Ty↓ T Γ↓

Ty↓ (π A B) Γ↓ = (a↓ : Ty↓ A Γ↓)  Ty↓ B (Γ↓ , A)
Ty↓ ‘⊤’ Γ↓ = ⊤
Ty↓ ‘⊥’ Γ↓ = ⊥
Ty↓ (W X) (Γ↓ , T) = Ty↓ X Γ↓
Ty↓ (B ⍟ a) Γ↓ = Ty↓ B (Γ↓ , Tm↓ a Γ↓)

Tm↓ (lam A b) Γ↓ = λ (a : Ty↓ A Γ↓)  Tm↓ b (Γ↓ , a)
Tm↓ (f ⊛ x) Γ↓ = Tm↓ f Γ↓ (Tm↓ x Γ↓)
Tm↓ tt = tt
Tm↓ (exf contra) Γ↓ = ⊥-rec (Tm↓ contra Γ↓)
Tm↓ (var v) Γ↓ = var↓ v Γ↓ -- variable lookup will be defined as an auxiliary function
  where var↓ :  {Γ T}  Γ ∋ T  (Γ↓ : Con↓ Γ)  Ty↓ T Γ↓
        var↓ top (Γ↓ , a) = a
        var↓ (pop v) (Γ↓ , b) = var↓ v Γ↓

This language is far from ideal for lots of reasons, but it serves to illustrate the basic concept. We can define a well typed syntax for dependent type theory fairly easily using induction induction, and the interpreter is a natural outgrowth of the language structure.

But I’ve been playing around lately with languages that have access to their own interpreter, and three different interpretation functions across three different type families sounds like an awful lot of stuff. Luckily, like every other time I have a problem, Conor McBride’s figured out a trick using induction recursion that gets me at least part of the way there.

First, the language representation:

data Label : Set where
  con ty tm : Label

I : Label  Set
data D : (: Label)  I ℓ  Set

I con = ⊤
I ty = D con _
I tm = Σ[ Γ ∈ D con _ ] D ty Γ

Con : Set
Con = D con _
Ty : Con  Set
Ty = D ty
__ :  Γ  Ty Γ  Set
Γ ⊢ T = D tm (Γ , T)

data D where
  -- Contexts
  ε : Con -- D con _
  _,_ :  Γ  Ty Γ  Con -- (Γ : D con _) → D ty Γ → D con _
  -- Types
  π :  {Γ} (A : Ty Γ)  Ty (Γ , A)  Ty Γ
  ‘⊤’ ‘⊥’ :  {Γ}  Ty Γ
  W :  {Γ A}  Ty Γ  Ty (Γ , A) -- weakening
  __ :  {Γ A} (B : Ty (Γ , A))  Γ ⊢ A  Ty Γ -- application at type level
  -- Terms
  top :  {Γ A}  (Γ , A) ⊢ W A
  pop :  {Γ A B}  Γ ⊢ A  (Γ , B) ⊢ W A
  lam :  {Γ} A (B : Ty (Γ , A))  (Γ , A) ⊢ B  Γ ⊢ π A B
  __ : A B}  Γ ⊢ π A B  (a : Γ ⊢ A)  Γ ⊢ B ⍟ a
  tt :  {Γ}  Γ ⊢ ‘⊤’ -- ‘⊤’ is terminal
  exf :  {Γ A}  Γ ⊢ ‘⊥’  Γ ⊢ A -- ‘⊥’ is initial

The interpreter itself will look an awful lot like the one we defined above, but with an added auxiliary function to specify the output type.

U₁ : Set
U₁ = NextU U ⟦_⟧ᵤ

⟦_⟧₁ : U₁  Set_⟧₁ =_⟧ₙ

I↓ :  I ℓ  U₁
⟦_:  {ℓ i}  D ℓ i  ⟦ I↓ ℓ i ⟧₁

I↓ con _ = uu
I↓ ty Γ = πu (up ⟦ Γ ⟧)  Γ↓  uu)
I↓ tm (Γ , T) = up (π ⟦ Γ ⟧ ⟦ T ⟧)

⟦ ε ⟧ = ⊤
⟦ Γ , T ⟧ = Σ ⟦ Γ ⟧ ⟦ T ⟧
⟦ ...= ... -- the rest of the function proceeds exactly as before

So, to review, using induction recursion we managed to specify an entire language, including contexts, types, and terms, as one giant inductive-recursive family, and write an interpreter for all three at once. I managed to use this alongside the trick for encoding very dependent types in Agda to write a language that parametrizes over its own interpreters, but didn’t actually find much use for it. Right now I’m looking at porting this trick to the Outrageous Coincidences representation to see if I can get around some of the issues I had that way, but I’m not particularly optimistic. My main enemy right now is the positivity restriction, which keeps me from doing quite as much with quotation as I’d like, as well as the standard issues about well typed dependent syntaxes.

Next post will either be another exploratory piece like this one or (if I manage to finish it) the post about introductions to type theory I promised last time.