Notes on Conor McBride unpublished Type-Preserving Renaming and Substitution

These notes are written in order to understand the paper and the programming technique presented in it. There is nothing original in these notes.

In this paper, Conor McBride proposes a programming technique that allows to define in a generic manner renaming and substitution on a lambda calculus defined ad an inductive type family (Altenkirch and Reus 1999).

The code in the original paper is written in Epigram. I translate it in Agda. I mostly keep the original names and notations.


module TPRS where

Firstly, let's define the priorities for the mixfix operators we are going to use.


infixr 10 _▷_
infixl  7 _,_
infix   5 _▶_
infix   5 _∋_

Then we define an inductive data type for the types of our object language. The simply types lambda calculus has only two types, a ground type and the function type, the latter being defined inductively.


data Ty : Set where
  ∙ : Ty
  _▷_ : Ty -> Ty -> Ty

As the variables in our object language are either bound by a binder (a lambda abstraction) or in a context, we need to define a data type of context which is just a list of types as we use De Bruijn indexes. In a similar way than the traditional presentation of context, we use a snoc list.


data Ctxt : Set where
  ε : Ctxt
  _,_ : Ctxt -> Ty -> Ctxt

In order to code De Bruijn indexes, we use membership proof that are isomorphic to Peano natural numbers.


data _∋_ : Ctxt -> Ty -> Set where
  vz : ∀ {Γ T}
    ------------
    -> Γ , T ∋ T
    
  vs : ∀ {Γ T S}
    -> Γ ∋ T
    ------------
    -> Γ , S ∋ T

We now can define the syntax of our object language. A term depends on the context in which it is valid and its type. This allows us to trust the host language's typechecker (agda's typechecker) to maintain the terms well-scoped and well-typed.


data _▶_ : Ctxt -> Ty -> Set where
  var : forall {Γ T}
    -> Γ ∋ T
    --------
    -> Γ ▶ T
    
  lda : forall {Γ T S}
    -> Γ , S ▶ T
    ------------
    -> Γ ▶ T ▷ S
    
  app : forall {Γ T S}
    -> Γ ▶ T ▷ S
    -> Γ ▶ T
    --------
    -> Γ ▶ S

Now that our object language is defined, we can tackle the main object of the paper: the generic definition of renaming and substitution of our De Bruijn terms.

The idea is to pack all the functions we'll need for renaming and substitution in a Kit so that we will be able to define a traversal operation that is parametrised by a Kit.

A Kit is a type family indexed by a type constructor (_◆_) that takes a context and a type. In the framework already defined, this is the type of the membership proof (_∋_), ie a variable identifier, and of a term (_▶_).

In order to understand the Kit idea, it is important to note that both renaming and substitution operate a traversal of a term replacing variables by something. Renaming replaces variables by variables and substitution replaces variables by terms. So the general idea is that _◆_ can be either a variable identifier or a term.

The vr field contains a function that turns a variable identifier into the corresponding stuff, either a variable identifier or a term. It is used in the lift function (called whenever we cross a binder in our traversal) in the case we cross the variable binded to the closest binder (the latest abstraction). In the case of renaming, vr is defined as the identity function as we don't want to rename the new bound variable. In the case of substitution, is is defined as var as we don't want to susbtitute the new bound variable.

The tm field contains a function that turns one of this diamond stuff into a term. It is used in the traversal operation whenever we cross a variable, its identifier first being turned into a diamond stuff by a given map (τ).

In the case of renaming, tm is defined as the var term constructor. The diamond stuff being variable identifier, we want to build a variable with a transformed variable identifier. We note that rename is only used to defined wk of the Kit for the subst function, the given map being vs, that is the equivalent of the successor constructor for membership proof. In fact, renaming is basically the operation of incrementing the De Bruijn indexes.

In the case of substitution, tm is defined as the identity function, all the work is done by the second argument, the map that turns variable identifiers into terms.

The wk field contains, as its name may suggest, a weakening function, that is a function that add a new type to the context with the guaranty of type preservation of the diamond stuff.

wk is only used in the second case of the lift function. That makes sense because the lift function weakens, so to speak, a type preserving map that turns a variable indentifier in a context Γ to a diamond stuff in context Δ.


record Kit (_◆_ : Ctxt -> Ty -> Set) : Set where
  constructor kit
  field
    vr : ∀ {Γ T} -> Γ ∋ T -> Γ ◆ T
    tm : ∀ {Γ T} -> Γ ◆ T -> Γ ▶ T
    wk : ∀ {Γ T S} -> Γ ◆ T -> (Γ , S) ◆ T

The lift function is a general weakening function called whenever we cross a binder in the traversal.


lift : ∀ {Γ Δ T S}{_◆_ : Ctxt -> Ty -> Set} -> Kit _◆_ -> (∀ {X} -> Γ ∋ X -> Δ ◆ X) -> Γ , S ∋ T -> (Δ , S) ◆ T
lift (kit vr tm wk) τ vz = vr vz
lift (kit vr tm wk) τ (vs x) = wk (τ x)

The traversal takes a Kit and a function that turns variable identifiers into diamond stuff (and of course a term to traverse). It calls tm on the transformed variable identiers, lift the body of the lambda abstraction before traversing them and traverse recursively the two terms composing an application.


trav : ∀ {Γ Δ T}{_◆_ : Ctxt -> Ty -> Set} -> Kit (_◆_) -> (∀ {X} -> Γ ∋ X -> Δ ◆ X) -> Γ ▶ T -> Δ ▶ T
trav (kit vr tm wk) τ (var x) = tm (τ x)
trav K τ (lda t) = lda (trav K (lift K τ) t) 
trav K τ (app t t₁) = app (trav K τ t) (trav K τ t₁)

rename traverse a term incrementing every De Bruijn indexes that need to be.


rename : ∀ {Γ Δ T} -> (∀ {X} -> Γ ∋ X -> Δ ∋ X) -> Γ ▶ T -> Δ ▶ T
rename ρ t = trav (kit (λ x -> x) var vs) ρ t

subst traverse a term replacing variable accordingly to a substitution map (σ)


subst : ∀ {Γ Δ T} -> (∀ {X} -> Γ ∋ X -> Δ ▶ X) -> Γ ▶ T -> Δ ▶ T
subst σ t = trav (kit var (λ x → x) (rename vs)) σ t