Tutorial

Proofs about the simply-typed lambda-calculus

1. Type uniqueness

The simply-typed lambda-calculus as we have defined above has the property that every lambda-term has a unique type. This property is called type uniqueness and can be stated as follows:

Theorem: If Γ ⊢ type_of E T and Γ ⊢ type_of E T' then ⊢ equal T T'.

We explain here how to implement the type uniqueness proof for the simply-typed lambda-calculus. If you'd like to run this example using Beluga, please download unique.bel, which contains all the data-type definitions and the implementation of the proof as a recursive function.

Equality reasoning is not built-in in the logical framework LF, but we can easily specify it.

datatype equal : tp → tp → type =
| e_ref : equal T T
;

This statement of type uniqueness makes explicit the context Γ containing typing assumptions about variables. Note that while terms E can depend on variables declared in Γ, no variables can occur in the types T and T', though this is not captured informally.

The theorem corresponds to a type of a recursive function in Beluga. Before showing how to implement it, we describe more precisely the shape of contexts Γ, using a context schema declaration:

schema tctx =  some [t:tp] block x:exp. type_of x t;

The schema tctx describes a context containing assumptions x:exp, each associated with a typing assumption type_of x t for some type t. In other words, concrete declarations in a context of schema tctx may instantiate the type t with some concrete type. Formally, we are using a dependent product type or Σ-type to tie x to type_of x t. We thus do not need to establish separately that for every variable there is a unique typing assumption: this is inherent in the definition of tctx.

We can now state the Beluga type corresponding to the statement above:

  (g:tctx) [g. type_of (E ..) T] → [g. type_of (E ..)  T')] → [ . eq T T'] 

We can read it as follows: for all contexts g of schema tctx, given derivations of [g. type_of (E ..) T] and of [g. type_of (E ..) T'] we can construct a derivation of [ . eq T T']. Quantification over the context variable g is written using curly brackets in {g:tctx}. We treat the context variable g as an implicit argmument, i.e., we do not have to pass g explicitely but the correct context will be inferred. To mark that context variables are explicitely passeed, we annotate the context schema name with * and write {g:(tctx)*} instead.

The [ ] means the result is closed. As we remarked, only the term E can contain variables; the type T is closed. Although we did not state this dependency in the on-paper statement, Beluga distinguishes closed objects from objects depending on assumptions. To describe the dependency of terms on the context, we write (E ..) associating .. with the variable E. (Technically, .. is an identity substitution mapping variables from g to themselves.) In contrast, T by itself denotes a closed tp that cannot depend on hypotheses in g.

Implementation of the proof for type uniqueness as a recursive function

In Beluga, inductive proofs are written as recursive programs. Case analysis and inversion in the on-paper proof correspond to case analysis and pattern matching in Beluga. Inversion is often represented using a let expression in Beluga, and is treated as a special instance of a case expression, i.e., a case expression with only one branch.

rec unique:(g:tctx) [g. type_of (E ..) T]  → [g. type_of (E ..) T'] 
	     → [ . equal T T'] = 
fn d ⇒ fn f ⇒ case d of
| [g. t_app (D1 ..) (D2 ..)] ⇒                      % Application Case 
  let [g. t_app (F1 ..) (F2 ..)] = f in 
  let [ . e_ref]  = unique [g. D1 ..] [g. F1 ..] in    
    [ . e_ref]   						 

| [g. t_lam (\x.(\u. D .. x u)] ⇒                 % Abstraction Case
  let [g. t_lam (\x.(\u. F .. x u)] = f in 
  let [ . e_ref] = unique [g,b: block x:exp. type_of x _ . D .. b.1 b.2] 
                         [g,b. F .. b.1 b.2] in 
   [ . e_ref]  

| [g. #q.2] ..% d : type_of #p.1 T    % Assumption Case 
  let [g. #r.2] .. = f in  % f : type_of #p.1 T'
    [ . e_ref] 			  
;

Application case: If d concludes with t_app, it matches the pattern [g. t_app (D1 ..) (D2 ..)], and is a contextual object of type type_of (app (E1 ..) (E2 ..)) T in the context g. We therefore know that E .. = app (E1 ..) (E2 ..).

Pattern matching also serves to invert the second argument, the derivation f which must have type [g. type_of (app (E1 ..) (E2 ..)) T')]. This is achieved through a let-binding. The appeal to the induction hypothesis using D1 and F1 in a paper proof corresponds to the recursive call unique [g. D1 ..] [g. F1 ..].

Note that we pass three arguments: the context g and contextual objects [g. D1 ..] and [g. F1 ..] describing the derivations of [g. type_of (E1 ..) (arr T2 T)] and [g. (type_of (E1 ..) (arr T2' T')]. The result is a contextual object of type [ . eq (arr T2 T) (arr T2' T')]. The only rule that could derive such an object is e_ref, and pattern matching establishes that arr T2 T = arr T2' T'. Since we know that T = T', there is a proof of [ . eq T T'].

Abstraction case: If the first derivation d concludes with t_lam, it matches the pattern [g. t_lam (\x.\u. D .. x u)], and is a contextual object in the context g of type [g. type_of (lam T1 (\x. E0 .. x)) (arr T1 T2)]. Thus, E .. = lam T1 (\x. E0 .. x) and T = arr T1 T2. Pattern matching—through a let-binding—serves to invert the second derivation f, which must have been by t_lam with a subderivation F1 .. x u of type_of (E0 .. x) T2' that can use x, u:type_of x T1, and assumptions from g. Hence, after pattern matching on d and f, we have E = lam T1 (\x. E0 .. x) and T = arr T1 T2 and T' = arr T1 T2'.

The use of the induction hypothesis on D and F in a paper proof corresponds to the recursive call to unique. To appeal to the induction hypothesis, we need to extend the context by pairing up x and its typing assumption: g,b:block x:exp.type_of x T1. In the code, we wrote an underscore _ instead of T1, which tells Beluga to reconstruct it. To retrieve x we take the first projection b.1, and to retrieve its typing assumption we write b.2.

Now we can appeal to the induction hypothesis using D1 .. b.1 b.2 and F1 .. b.1 b.2 in the context g,b:block x:exp. type_of x T1. We pass three arguments: the context g and two contextual objects. From the i.h. we get a contextual object, a closed derivation of [ . eq (arr T1 T2) (arr T1 T2')]. The only rule that could derive this is e_ref, and pattern matching establishes that T2 must equal T2', and hence arr T1 T2 = arr T1 T2', i.e. T = T'. Hence, there is a proof of [ . eq T T'], and we can finish the case with the reflexivity rule e_ref.

Assumption case Here, we must have used an assumption from the context g to construct the derivation d. Parameter variables #q allow a generic case that matches a declaration block x:exp.type_of x S for any S in g. Since our pattern match proceeds on typing derivations, we want the second component, written #q.2. The pattern match on d also establishes that E = #q.1 and S = T. Next, we pattern match on f, which has type type_of (#q.1 ..) T' in the context g. Clearly, the only possible way to derive f is by using an assumption from g. We call this assumption #r, standing for a declaration block y:exp.type_of y S', so #r.2 refers to the second component type_of (#r.1 ..) S'. Pattern matching between #r.2 and f also establishes that both types are equal and that S' = T' and #r.1 = #q.1. Finally, we observe that #r.1 = #q.1 only if #r is equal to #q. Consequently, both parameters have equal types, and S = S' = T = T'. (In general, unification in the presence of Σ-types does not yield a unique unifier, but in Beluga only variables from the context and parameter variables can have Σ-types, yielding a unique solution.)

2. Reasoning about paths in lambda-terms

3. Subject reduction

4. Equality reasoning

5. Reasoning about smaller relation on lambda-terms