2.3 Examples
2.3.1 Example: Vector Append
In the introduction we used the example of proving that vector append is associative to illustrate some of the problems with how equality is represented in typical dependently typed languages. We will now revisit this example to examine how Theta handles it in detail.
We begin with natural numbers and plus
data Nat : Type where
Zero
Succ of (x : Nat)
log plus : Nat→ Nat→ Nat
ind plus n m =
case n of
Zero →m
Succ n’→Succ (plus n’ ord m)
These definitions are standard except for the extra argument “ord” in the recursive call of plus. To understand this argument, recall the rule for terminating recursion inTheta:
Γ`L (y :A)→ B :Type
`
Γ,y :LA,f :L(x :A)→ (z :x <y)→ [x/y]B `Lb :B
Γ`Lindf y.b : (y :A)→ B TInd
When checking the body of an inductive function, the recursive call f (here plus) must be provided with the extra argument z—a proof that the expression on which f is called is a subterm of the original input. Here, in particular, we need to show that n’ < n. Such proofs are constructed by ord:
Γ`L a :b =d 1bi i∈1...j Γ`L ord:b i <b TOrd
So we can see that for ord to typecheck here, it would be enough to have a proof of
n = Succ n’. Happily, our rule for pattern matching adds exactly such an equality into the context when checking this branch (as the variable y in the final premise below):
Γ`L a :Da
ii Γ`LB :Type`2
dataD∆ : Type`1where{diof∆i i∈1...k } ∈Γ ∀i ∈1...k, Γ,[aii/∆]∆iθ,y :La =di∆i `θ bi :B Γ`θ caseaof{d i∆i ⇒bi i∈1...k }:B TCase
In the concrete syntax of Zombie, this reasoning is made more explicit by naming the equality proof provided by pattern matching and supplying it as an argument to
ord. But Zombie terms erase to core Theta terms before evaluation, so we obtain the program here that closely resembles the typical addition function.
With natural numbers in hand, we are prepared to represent vectors. As men- tioned in Section 2.2.4, coreTheta datatypes have parameters but no indices. Typ- ically the length of a vector is represented with an index, so our definition of vectors is slightly unusual:
data Vec (A : Type) (n : Nat) : Type where
Nil of {n = 0}
Cons of {sz : Nat} {n = Succ sz} A (Vec A sz)
Here we simulate indices by adding extra arguments to the constructors Nil and
Cons. In the former case, we demand a proof that the length is zero, and in the latter case we demand a proof that the length is one greater than the length of the tail.
Carrying around these proofs at run-time would be inefficient, soZombieincludes another feature: ICC∗-style “compile-time only” annotations [7, 43]. This allows the
user to tag these arguments as irrelevant at run time. We have not modeled this feature in Theta, so we will keep the proofs as real arguments but mark them as implicit since they are typically uninteresting and inferable.
We can define append for vectors inductively:
log app : {A : Type}→ {n m : Nat}
→Vec A n→Vec A m→ Vec A (plus n m)
ind app v1 v2 =
case v1 of
Nil → v2
Cons a v1’→ Cons a (app v1’ ord v2)
There is quite a bit of implicit equality reasoning needed to typecheck this pro- gram. Consider the Nil branch of the pattern match. We are expected to return a term of type Vec A (plus n m), but have supplied vswhose type is Vec A m. To see why this typechecks, recall from our definition of vectors that Nil actually carries with it a proof that the vector’s length is zero, so when checking this clause of the match we have a proof of n = 0 in the context. Additionally, the rule TRefl may be used to obtain a proof of plus 0 m = m:
Γ`L a 1 =a2 :Type` a1 V∗ b a2 V∗ b Γ`L refl:a 1 =a2 TRefl
We can put these proofs together using rule TConv to givev2 the desired type. Γ`θ a : [b 1/x]A Γ`L b :b 1 =b2 Γ`L [b 2/x]A:Type` Γ`θ a : [b 2/x]A TConv
In particular, one use of TConv changes the type (Vec A m) to (Vec A (plus 0 m)) using the proof from refl, and a second use of TConv changes the type (Vec A (plus 0 m)) to (Vec A (plus n m)) using the proof from Nil. In the cur- rent version of the source language Zombie, the typechecker is able to infer both uses of conversion if the user provides the hint that plus 0 m should be reduced. Regardless of how much annotation is required in the source, however, the program is erased to the version shown here.
We would like to prove that vector append is associative, as described in the introduction. As we observed there, Theta’s heterogeneous equality allows us to state and prove this theorem directly:
log app_assoc : {A : Type}→ {n m k : Nat}
→(v1 : Vec A n)→(v2 : Vec A m)→(v3 : Vec A k) →app v1 (app v2 v3) = app (app v1 v2) v3
ind app_assoc v1 v2 v3 =
case v1 of
Nil → refl
Cons a v1’→ refl
Although typechecking this proof requires substantial equality reasoning, including a recursive call to app_assoc, we can see that none of this is reflected in the core Theta term itself because conversion is unmarked. To demonstrate how conversion is being used here, we will walk through how theConscase of this program typechecks in detail.
Begin by observing that, by TRefl, we have:
refl : app v1 (app v2 v3) = app v1 (app v2 v3)
We will use this proof as a starting point and convert it to the desired type via multiple uses of TConv. Recall that Theta’s pattern matching rule will provide us with a proof ofv1 = Cons a v1’, so we may use conversion to obtain:
refl : app v1 (app v2 v3) = app (Cons a v1’) (app v2 v3)
We would like to unfold the definition ofappon the right hand side of this equality. SinceTheta is a call-by-value language, this expression is stuck andTReflwill not reduce it. However, we can still achieve the desired result via lambdas. First, observe that:
\x . refl : (x : Vec A k)→app (Cons a v1’) x = Cons (a (app v1’ x))
This equality typechecks where the other did not becausexis a value and thusTRefl can reduce the use of app. We may apply this function to obtain:
(\x.refl) (app v2 v3)
: app (Cons a v1’) (app v2 v3) = Cons a (app v1’ (app v2 v3))
Using this equality and TConv with our previous equality, we obtain:
refl : app v1 (app v2 v3) = Cons a (app v1’ (app v2 v3))
At this point, we would like to make a recursive call to get an equality involving the tail of the right-hand side. In particular:
app_assoc v1’ ord v2 v3 : app v1’ (app v2 v3) = app (app v1’ v2) v3
Note that, as we saw before, the recursive call must be supplied with a proofordthat the new argument is smaller than the original input. Specifically, here ordmust have the type v1’ < v1. But pattern matching provides a proof of v1 = Cons a v1’, so TOrdcan give ordthe desired type.
Using the result of the recursive call and TConv with the previous equality, we obtain:
Now we would like to fold the definition ofappback up in order to obtain the desired result. We may use the same eta-expansion trick we used for the previous step of reduction to prove:
Cons a (app (app v1’ v2) v3) = app (app (Cons a v1’) v2) v3
Using this equality and TConv with the previous equality, we obtain:
refl : app v1 (app v2 v3) = app (app (Cons a v1’) v2) v3
And since pattern matching provided a proof of v1 = Cons a v1’, a final use of TConv brings us to the desired equality:
refl : app v1 (app v2 v3) = app (app v1 v2) v3
It is clear that choosing not to mark uses of conversion in the syntax of expressions is buying us substantially simpler core programs. In the description of how just the
Cons branch of the app_assoc theorem typechecks we usedTConv five times. In a language like Coq, most of these require an explicit appeal to an equality eliminator in the proof itself.
In the remaining examples we will not describe how the programs typecheck in such detail. The curious reader may refer to the annotated versions that were typechecked with Zombie, where some of this reasoning is made explicit.