Though these examples may make sense more fully after reading the sections below, it may be helpful at this point to see a few short examples of Picoprograms.
We will work with a definition of length-indexed vectors, a tried-and-true example of the design of GADTs. Here is how they are declared in Haskell (further explanation is available in Section 3.1.1):
dataNat =Zero |Succ Nat
dataVec::Type→Nat →Typewhere VNil ::Vec a0
VCons ::a →Vec a n→Vec a(’Succ n)
If Pico had a concrete syntax, these declarations would be transformed roughly into the following:
Nat ::Type
Zero ::Nat
Succ ::Nat →Nat
Vec ::Type→Nat →Type
VNil ::∀(a::Type) (n::Nat).(n ∼ Zero)→Vec a n VCons::∀(a::Type) (n::Nat).
∀(m::Nat).(n ∼ Succ m)→a→Vec a m→Vec a n
The change seen here is just the transformation between specifying a GADT equality constraint via a return type in a declaration to using an explicit existential variable with an explicit equality constraint.
In the abstract syntax of Pico, these declarations are represented by this signature
Σ0:
Σ0 =Nat:(∅),
Zero:(∅;Nat),
Succ:(_:RelNat;Nat),
Vec:(a:Type,n :Nat),
VNil:(c:n ∼0;Vec),
VCons:(m:IrrelNat,c:n ∼ Succ m,_:Rela,_:RelVec a m;Vec)
Let’s walk through these declarations. Our binding for Nat includes an empty list of universally quantified type variables. This binding is followed by specifications for
Zero, which lists no existential variables and is a constructor of the datatype Nat, and
Succ, which has one (anonymous) existential variable and also belongs to Nat. The bindings forVec and its constructors are similar, but with more parameters. Note the coercion bindings in the telescopes associated with VNil and VCons, as well as the irrelevant binding for the existential m ofVCons. The design we see here, echoing the Haskell, does not permit runtime extraction of the length of a vector. If we changed the m to be relevant, then runtime length extraction would be trivial.
We will now look at a few simple operations on vectors, first in Haskell and then inPico.51
5.5.1
isEmpty
First, a very simple test for emptiness, in order to familiarize ourselves with pattern- match syntax in Pico:
isEmpty ::Vec a n→Bool isEmpty VNil =True isEmpty (VCons { }) =False
Translated to Pico, we get the following:
isEmpty : ˜
Π(a:IrrelType),(n:IrrelNat),(v:RelVec a n).Bool
isEmpty=λ(a:IrrelType),(n:IrrelNat),(v:RelVec a n).
caseBoolvof
VNil →λ(c:n ∼0),(c0:v ∼ VNil{a,n}c).True
VCons→λ(m:IrrelNat),(c:n ∼ Succ m),(x:Rela),(xs:RelVec a m),
(c0:v ∼ VCons{a,n}mcx xs).
False
The most striking feature about this Pico code is the form of the case expression. Unlike the concrete syntax of Haskell, patterns in Pico do not directly bind any 51In these examples, I assume the use of numerals to specify elements of type Nat, and I also
arguments. Note that there are no variable bindings to the left of the arrows in the case-branches. Instead, I have chosen to have λs to the right of the arrow. This design choice greatly simplifies the typing and scoping rules for pattern matches, because it removes a binding site in the grammar (leaving us with two: Πand λ). Because of the typing rule forcaseexpressions (Section 5.6.5), westill must bind all of the existentials of a data constructor when matching against it—even when these existentials are ignored, as we see here.
The matches also bind a variable not mentioned in the data constructors’ exis- tentials: the coercion variable c0. This coercion witnesses the equality between the
scrutinee (v, in this case) and the applied data constructor that introduces the case branch. This coercion variable is bound in all matches, meaning that all pattern matching in Picois dependent pattern matching.52
The behavior of case can also be viewed through its operational semantics, as captured in the following rule, excerpted from Section 5.7.2:
alti = H →τ0
Σ; Γ`scaseκH{τ}ψofalt −→τ0ψhH{τ}ψi
S_Match
Note that the body of the match, τ0, is applied to the existential arguments toH{τ} and a coercion witnessing the equality between the scrutinee and the pattern. In the case of a successful match, this coercion is reflexive, as denoted by the angle brackets
hH{τ}ψi.
5.5.2
replicate
Let’s now look atreplicate, one of the simplest functions that requires a properΠ-type. First, in Haskell:
replicate:: Πn →a→Vec a n replicate Zero =VNil
replicate (Succ m)x =VCons x (replicate m x)
52Contrast to Gundry [37], who use two separate constructs, case anddcase, only the latter
of which does dependent matching. This separation is necessary in his language because not all expressions can be used in types and thus in dependent pattern matching. In particular, Gundry preventsλ-expressions in types, a limitation I have avoided by maintaining the distinction between matchable and unmatchableΠ-types.
Now, in Pico:
replicate : ˜
Π(a:IrrelType),(n:RelNat),(x:Rela).Vec a n
replicate=λa:IrrelType.
fixλ(r:Rel
˜
Π(n:RelNat),(x:Rela).Vec a n),
(n:RelNat),(x:Rela).
caseVec a nnof
Zero →λc0:(n ∼Zero).VNil{a,n}c0
Succ→λm:RelNat,c0:(n ∼Succ m).VCons{a,n}{m}c0x(r m x)
This example shows the (standard) use of fix as well as some of the more exotic features of Pico. In the case branches, we see how we pass universal arguments to the data constructors VNil and VCons. We also see how we have to wrap irrelevant arguments (the {m} in the last line) in braces. This example also shows where the coercion variable c0 comes into play: it’s needed to provide the coercion to the VNil
andVCons constructors to prove that the universal argument n is indeed of the shape required for these constructors. Without the ability to do a dependent pattern match, this example would be impossible to write, unless you fake dependent types using singletons or some other technique.
5.5.3
append
We’ll now examine how to append two vectors. This operation will also require the use of an addition operation, defined using prefix notation so as not to pose a parsing challenge:
plus::Nat →Nat →Nat plus Zero n=n
plus (Succ m)n=Succ (plus m n)
append::Vec a m→Vec a n→Vec a(’plus m n)
append VNil ys =ys
And in Pico (where I elide the uninteresting plus for brevity):
append : ˜
Π(a:IrrelType),(m:IrrelNat),(n:IrrelNat),(xs:RelVec a m),(ys:RelVec a n).
Vec a(plus m n)
append=λ(a:IrrelType).
fixλ(app:Rel
˜
Π(m:IrrelNat),(n:IrrelNat),(xs:RelVec a m),(ys:RelVec a n).
Vec a(plus m n)),
(m:IrrelNat),(n:IrrelNat),(xs:RelVec a m),(ys:RelVec a n).
caseVec a(plus m n)xsof
VNil →λ(c:m ∼ Zero),(c0:xs ∼VNil{a,m}c). letc1 :=hplusichniin
letc2 :=stepj(plus Zero n)in
ysBsym(Vechai(c1#c2))
VCons→λ(m’:IrrelNat),(c:m ∼ Succ m’),(x:Rela),(xs’:RelVec a m’)
(c0:xs ∼ VCons{a,m}{m’}cx xs’). letc1 :=hplusichniin
letc2 :=stepk(plus(Succ m’)n)in
VCons{a,plus m n}{plus m’ n}(c1#c2)x(app{m’} {n}xs’ ys)
This is the first example where we are required to write non-trivial coercions. Let’s start by considering the right-hand side of the VNil case. As we see in the Haskell version, we wish to return ys. However, ys has type Vec a n, and we need to return something of type Vec a(plus m n). We must, accordingly, cast ys to have type Vec a (plus m n). This is what the coercion sym(Vechai(c1 #c2)) is doing; it
proves that Vec a n is in fact equal to Vec a (plus m n). Both the starting type
Vec a n and the ending type Vec a (plus m n) have the same prefix of Vec a. We use a congruence coercion (Section 5.8.5) Vechaiγ to simplify our problem. Now, we need only a coercionγ that proves plus m n equalsn. (The use of sym helpfully has reversed our proof obligation.) This γ is built in two steps, tied together by using our transitivity operator#: c1, which uses our reflexivity operatorh·i, proves thatplus m n
equals plus 0n by usingc, the GADT equality constraint from the VNil constructor; andc2 proves that plus 0n equalsn.53 For this last coercion, we use thestepcoercion
that reduces a type by one step. It is fiddly (and unenlightening) to calculate the precise number of steps necessary to get from plus 0nto n, so I have just written that this takes j steps. It is straightforward to calculate j in practice.
The coercion manipulations in the VCons case are similar.
Also of note in this example is the interplay between relevant variables and irrelevant ones. We see that the lengths m andn are irrelevant throughout this function. Indeed, we do not need lengths at runtime to append two vectors. Accordingly, we can see that all uses of m andn (orm’) occur in irrelevant contexts, such as coercions or irrelevant arguments to functions.
53Recall (Figure 5.2 on page 77) that let is defined by simple expansion. It is not properly a
5.5.4
safeHead
With length-indexed vectors, we can write a safe head operation, allowed only when we know that the vector has a non-zero length:
safeHead ::Vec a(’Succ n)→a safeHead (VCons x ) = x
Note thatsafeHead contains a total pattern match; the VNil alternative is impossible given the type signature of the function. This function translates to Picothusly:
safeHead : ˜
Π(a:IrrelType),(n:IrrelNat),(v:RelVec a(Succ n)).a
safeHead=λ(a:IrrelType),(n:IrrelNat),(v:RelVec a(Succ n)).
caseavof
VNil →λ(c:Succ n ∼ Zero),(c0:v ∼ VNil{a,Succ n}c).absurdca
VCons→λ(m:IrrelNat),(c:Succ n ∼Succ m),(x:Rela),(xs:RelVec a m),
(c0:v ∼VCons{a,Succ n}{m}cx xs).
x
The new feature demonstrated in this example is the absurd operator, which appears in the body of the VNil case. In order to be sure that case expressions do not get stuck, the typing rules require that all matches are exhaustive. However, in general, in can be undecidable to determine whether the type of a scrutinee indicates that a certain constructor can be excluded. In order to step around this potential trap, Pico supports absurdity elimination through absurd. The coercion passed into absurd (c, above) must prove that one constant equals another. This is, of course, impossible, and so we allow absurdγ τ to have any type τ.