100% found this document useful (1 vote)
28 views12 pages

An Implementation of A Dependently Typed Lambda Calculus

Uploaded by

Xenakis
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (1 vote)
28 views12 pages

An Implementation of A Dependently Typed Lambda Calculus

Uploaded by

Xenakis
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 12

Simply Easy!

An Implementation of a Dependently Typed Lambda Calculus

Andres Löh Conor McBride Wouter Swierstra


University of Bonn University of Nottingham
[email protected] [email protected] [email protected]

Abstract less than 150 lines of Haskell code, and can be generated directly
We present an implementation in Haskell of a dependently-typed from this paper’s sources.
lambda calculus that can be used as the core of a programming The full power of dependent types can only show if we add
language. We show that a dependently-typed lambda calculus is no actual data types to the base calculus. Hence, we demonstrate in
more difficult to implement than other typed lambda calculi. In fact, Section 4 how to extend our language with natural numbers and
our implementation is almost as easy as an implementation of the vectors. More data types can be added using the principles ex-
simply typed lambda calculus, which we emphasize by discussing plained in this section. Using the added data types, we write a few
the modifications necessary to go from one to the other. We explain example programs that make use of dependent types, such as a vec-
how to add data types and write simple programs in the core tor append operation that keeps track of the length of the vectors.
language, and discuss the steps necessary to build a full-fledged Programming in λ5 directly is tedious due to the spartan nature
programming language on top of our simple core. of the core calculus. In Section 5, we therefore sketch how to
proceed if we want to construct a real programming language on
top of our dependently-typed core. Many aspects of designing a
1. Introduction dependently-typed programming language in which one can write
Most Haskell programmers are hesitant to program with dependent large, complex, programs, are still subject of ongoing research.
types. It is said that type checking becomes undecidable; the phase While none of the type systems we implement are new, we be-
distinction between type checking and evaluation is irretrievably lieve that our paper can serve as a gentle introduction on how to
lost; the type checker will always loop; and that dependent types implement a dependently-typed system in Haskell. The λ5 calcu-
are just really, really, hard. lus has the nature of an internal language: it is explicitly typed,
The same Haskell programmers, however, are perfectly happy to requires a lot of code that one would like to omit in real programs,
program with a ghastly hodgepodge of generalized algebraic data and it lacks a lot of syntactic sugar. However, the language being
types, multi-parameter type classes with functional dependencies, explicit also has its merits: writing simple dependently-typed pro-
impredicative higher-ranked types, and even data kinds. They will grams in it can be very instructive and reveal a lot about the be-
go to great lengths to avoid dependent types. haviour of dependently-typed systems. We have therefore included
This paper aims to dispel many misconceptions Haskell pro- code in the paper sources that provides a small interpreter around
grammers may have about dependent types. We will present and the type system and evaluator we describe, together with a few ex-
explain a dependently-typed lambda calculus λ5 that can serve as ample functions, to serve as a simple environment to play with and
the core of a dependently-typed programming language, much like perhaps extend. If you want more, we have included pointers in the
Haskell can be based on the polymorphic lambda calculus Fω . We conclusions (Section 6) to more advanced programming environ-
will not only spell out the type rules of λ5 in detail, but also pro- ments that allow dependently-typed programming right now.
vide an implementation in Haskell.
To set the scene, we examine the simply-typed lambda calcu- 2. Simply Typed Lambda Calculus
lus (Section 2). We present both the mathematical specification Roughly speaking, Haskell’s type system can be divided into three
and Haskell implementation of the abstract syntax, evaluation, and levels: expressions, types and kinds. Programmers write expres-
type checking. Taking the simply-typed lambda calculus as start- sions, and the type checker ensures they are well-typed. The type
ing point, we move on to a dependently typed lambda calculus language itself is extremely rich. For instance, data types, such as
(Section 3). Inspired by Pierce’s incremental development of type lists, can abstract over the type of their elements. The type lan-
systems [21], we highlight the changes, both in the specification guage is so complex that the types themselves have types, called
and the implementation, necessary to shift to a dependently typed kinds. The kind system itself is relatively simple: all types inhab-
lambda calculus. Perhaps surprisingly, the modifications necessary ited by expressions have kind ∗; type constructors, such as lists,
are comparatively small. The resulting implementation of λ5 is have a ‘function kind’, in the same was as lambda expressions have
a ‘function type.’
With its three levels, Haskell is a rich version of the typed
lambda calculus called Fω , which also forms the basis of the core
language used in GHC. Compared to pure Fω , full Haskell is aug-
mented with lots of additional features, most notably the facility to
define your own data types and a cunning type inference algorithm.
In this section, we consider the simply-typed lambda calculus,
or λ→ for short. It has a much simpler structure than Fω as there
[Copyright notice will appear here once ’preprint’ option is removed.] is no polymorphism or kind system. Every term is explicitly typed

1 2007/6/18
e⇓v 0 ::= ε empty context
e :: τ ⇓ v x ⇓ x | 0, α :: ∗ adding a type identifier
e1 ⇓ λx → v1 e2 ⇓ v2 e1 ⇓ n1 e2 ⇓ v2 e⇓v | 0, x :: τ adding a term identifier
e1 e2 ⇓ v1 [x 7→ v2 ] e1 e2 ⇓ n1 v2 λx → e ⇓ λx → v valid(0) valid(0) 0 ` τ :: ∗
valid(ε) valid(0, α :: ∗) valid(0, x :: τ )
Figure 1. Evaluation in λ→
0(α) = ∗ 0 ` τ :: ∗ 0 ` τ 0 :: ∗
and no type inference is performed. In a sense, λ→ is the smallest 0 ` α :: ∗ 0 ` τ → τ 0 :: ∗
imaginable statically typed functional language. Figure 2. Contexts and well-formed types in λ→
2.1 Abstract syntax
The type language of λ→ consists of just two constructs: 0 ` τ :: ∗ 0 ` e ::↓ τ 0(x) = τ 0 ` e1 ::↑ τ → τ 0 0 ` e2 ::↓ τ
τ ::= α base type 0 ` (e :: τ ) ::↑ τ 0 ` x ::↑ τ 0 ` e1 e2 ::↑ τ 0
| τ → τ0 function type
0 ` e ::↑ τ 0, x :: τ ` e ::↓ τ 0
There is a set of base types α; compound types τ → τ 0 correspond
to functions from τ to τ 0 . 0 ` e ::↓ τ 0 ` λx → e ::↓ τ → τ 0

e ::= e :: τ annotated term1 Figure 3. Type rules for λ→


| x variable
| e1 e2 application
| λx → e lambda abstraction indicate that α is a base type, and x :: t to indicate that x is a term
There are four kinds of terms: terms with an explicit type annota- of type t. Every free variable in both terms and types must occur in
tion; variables; applications; and lambda abstractions. the context. For instance, if we want to declare const to be of type
Terms can be evaluated to values: (β → β) → α → β → β, we need our context to contain at least:
v ::= n neutral term α :: ∗, β :: ∗, const :: (β → β) → α → β → β
| λx → v lambda abstraction Note α and β are introduced before they are used in the type of
n ::= x variable const. These considerations motivate the definitions of contexts
| nv application and their validity given in Figure 2.
A value is either a neutral term, i.e., a variable applied to a (possibly Multiple bindings for the same variable can occur in a context,
empty) sequence of values, or it is a lambda abstraction. with the rightmost binding taking precedence. We write 0(z) to
denote the information associated with identifier z by context 0.
2.2 Evaluation The last two rules in Figure 2 explain when a type is well-
formed, i.e., when all its free variables appear in the context. In
Evaluation rules are given in Figure 1. The notation e ⇓ v means
the rules for the well-formedness of types as well as in the type
that e directly evaluates to v. The rules we present will evaluate a
rules that follow, we implicitly assume that all contexts are valid.
term to its normal form. As a result, unlike Haskell, we will con-
Note that λ→ is not polymorphic: a type identifier represents a
tinue to evaluate under a lambda. Type annotations are ignored dur-
specific type, and cannot be instantiated.
ing evaluation. Variables evaluate to themselves. The only interest-
Finally, we can give the (syntax-directed) type rules (Figure 3).
ing case is application. In this case, it depends whether the left hand
It turns our that for some expressions, we can infer a type, whereas
side evaluates to a lambda abstraction or to a neutral term. In the
generally, we can only check an expression against a given type.
former case, we β-reduce. In the latter case, we add the additional
The arrow on the type rule indicates whether the type is an input
argument to the spine.
(::↓ ) or an output (::↑ ). For now, this is only a guideline, but the
The evaluation rules given here are strict. In an application,
distinction will become more significant in the implementation.
we always evaluate the argument. This is different from Haskell’s
Let us first look at the inferable terms. We check annotated
non-strict semantics. However, the simply-typed lambda calculus
terms against their type annotation, and then return the type. The
is strongly normalizing: evaluation terminates for any term, and the
types of variables are looked up in the environment. For applica-
resulting value is independent of the evaluation strategy.
tions, we deal with the function first, which must be of a function
Here are few example terms in λ→ , and their evaluations. Let
type. We can then check the argument against the function’s do-
us write id to denote the term λx → x, and const to denote the
main, and return the range as the result type.
term λx y → x, which we use in turn as syntactic sugar for
The final two rules are for type checking. If we want to check
λx → λy → x. Then
an inferable term against a type, then this type must be identical
(id :: α → α) y ⇓ y to the one that is inferred for the term. A lambda abstraction can
(const :: (β → β) → α → β → β) id y ⇓ id only be checked against a function type. We check the body of the
abstraction in an extended context.
2.3 Type System Here are type judgements – derivable using the above rules –
for our two running examples:
Type rules are generally of the form 0 ` e :: t, indicating that a
term e is of type t in context 0. The context lists valid base types, α :: ∗, y :: α ` (id :: α → α) y :: α
and associates identifiers with type information. We write α :: ∗ to α :: ∗, y :: α, β :: ∗ ` (const :: (β → β) → α → β → β) id y :: β → β

1 Type theorists use ‘:’ or ‘∈’ to denote the type inhabitation relation. In
2.4 Implementation
Haskell, the symbol ‘:’ is used as the “cons” operator for lists, therefore the
designers of Haskell chose the non-standard ‘::’ for type annotations. In this We now give an implementation of λ→ in Haskell. We provide
paper, we will stick as close as possible to Haskell’s syntax. an evaluator for well-typed expressions, and routines to type-check

2 2007/6/18
λ→ terms. The implementation follows the formal description that type Env = [Value]
we have just introduced very closely.
eval↑ :: Term↑ → Env → Value
We make use of two simple implementation tricks that help us
eval↑ (Ann e ) d = eval↓ e d
to focus of the essence of the algorithms.
eval↑ (Par x) d = vpar x
De Bruijn indices In order to save us the work of implement- eval↑ (Var i) d = d !! i
ing α-conversion and α-equality – i.e., renaming of variables, pre- eval↑ (e1 :@: e2 ) d = vapp (eval↑ e1 d) (eval↓ e2 d)
venting name capture, etc. – we use De Bruijn indices: each oc-
vapp :: Value → Value → Value
currence of a bound variable is represented by a number indicat-
vapp (VLam f ) v=f v
ing how many binders occur between the variable and where it is
vapp (VNeutral n) v = VNeutral (NApp n v)
bound.
Using this notation, we can for example write id as λ → 0, and eval↓ :: Term↓ → Env → Value
const as λ → λ → 1. When using De Bruijn indices, the equality eval↓ (Inf i) d = eval↑ i d
check on types can be implemented as syntactic equality. eval↓ (Lam e) d = VLam (λx → eval↓ e (x : d))
While bound variables are represented using natural numbers,
we still make use of strings for free variables. The data type of Figure 4. Implementation of an evaluator for λ→
terms has a constructor Var, taking an Int as argument, for a De
Bruijn index, and a constructor Par with a Name argument, for Values are lambda abstractions (VLam) or neutral terms (VNeutral).
free variables. Most names are strings, we will introduce the other
categories when we need them: data Value
= VLam (Value → Value)
data Name | VNeutral Neutral
= Const String
| Bound Int As described in the discussion on higher-order abstract syntax, we
| Unquoted Int represent function values as Haskell functions of type Value →
deriving (Show, Eq) Value. For instance, the term const – if evaluated – results in the
value VLam (λx → VLam (λy → x)).
Higher-order abstract syntax We make use of the Haskell func- The data type for neutral terms matches the formal abstract
tion space to represent function values. With this representation, syntax exactly. A neutral term is either a variable (NPar), or an
we can implement function application using Haskell’s own func- application of a neutral term to a value (NApp).
tion application, and we do not have to implement β-reduction.
data Neutral
There is a small price to pay, namely that Haskell functions
= NPar Name
cannot be inspected, i.e., we cannot print them or compare them | NApp Neutral Value
for equality. We can, however, use our knowledge about the syntax
of values to quote values and transform the back into types. We will We introduce a function vpar that creates the value corresponding
return to the quote function, after we have to defined the evaluator to a free variable:
and type checker. vpar :: Name → Value
Abstract syntax The type rules in Figure 3 reveal that we can infer vpar n = VNeutral (NPar n)
the types for annotated terms, variables and application constructs, Evaluation The code for evaluation is given in Figure 4. The
whereas we can only check the type for lambda abstractions. We functions eval↑ and eval↓ implement the big-step evaluation rules
therefore make a syntactic distinction between inferable (Term↑ ) for inferable and checkable terms respectively. Comparing the code
and checkable (Term↓ ) terms. to the rules in Figure 1 reveals that the implementation is mostly
data Term↑ straightforward.
= Ann Term↓ Type Substitution is handled by passing around an environment of
| Var Int values. Since bound variables are represented as integers, the envi-
| Par Name ronment is just a list of values where the i-th position corresponds
| Term↑ :@: Term↓ to the value of variable i. We add a new element to the environment
deriving (Show, Eq) whenever evaluating underneath a binder, and lookup the correct el-
data Term↓ ement (using Haskell’s list lookup operator !!) when we encounter
= Inf Term↑ a bound variable.
| Lam Term↓ For lambda functions (Lam), we introduce a Haskell function
deriving (Show, Eq) and add the bound variable x to the environment while evaluating
Annotated terms are represented using Ann. As explained above, the body.
we use integers to represent bound variables (Var), and names for Contexts Before we can tackle the implementation of type check-
free variables (Par). The infix constructor :@: denotes application. ing, we have to define contexts. Contexts are implemented as (re-
Inferable terms are embedded in the checkable terms via the versed) lists associating names with either ∗ (HasKind Star) or a
constructor Inf , and lambda abstractions (which do not introduce type (HasType t):
an explicit variable due to our use of De Bruijn indices) are written
data Kind = Star
using Lam.
deriving (Show)
Types consist only of type identifiers (TPar) or function arrows
(Fun). We reuse the Name data type for type identifiers. In λ→ , data Info = HasKind Kind | HasType Type
there are no bound names on the type level, so there is no need for deriving (Show)
a TVar constructor. type Context = [(Name, Info)]
data Type Extending a context is thus achieved by the list “cons” operation;
= TPar Name looking up a name in a context is performed by the Haskell standard
| Fun Type Type list function lookup.
deriving (Show, Eq)

3 2007/6/18
kind↓ :: Context → Type → Kind → Result () subst↑ :: Int → Term↑ → Term↑ → Term↑
kind↓ 0 (TPar x) Star subst↑ i r (Ann e τ ) = Ann (subst↓ i r e) τ
= case lookup x 0 of subst↑ i r (Var j) = if i == j then r else Var j
Just (HasKind Star) → return () subst↑ i r (Par y) = Par y
Nothing → throwError "unknown identifier" subst↑ i r (e1 :@: e2 ) = subst↑ i r e1 :@: subst↓ i r e2
kind↓ 0 (Fun κ κ 0 ) Star subst↓ :: Int → Term↑ → Term↓ → Term↓
= do kind↓ 0 κ Star subst↓ i r (Inf e) = Inf (subst↑ i r e)
kind↓ 0 κ 0 Star subst↓ i r (Lam e) = Lam (subst↓ (i + 1) r e)
type↑ 0 :: Context → Term↑ → Result Type
type↑ 0 = type↑ 0 Figure 6. Implementation of substitution for λ→
type↑ :: Int → Context → Term↑ → Result Type
type↑ i 0 (Ann e τ )
= do kind↓ 0 τ Star quote0 :: Value → Term↓
type↓ i 0 e τ quote0 = quote 0
return τ quote :: Int → Value → Term↓
type↑ i 0 (Par x) quote i (VLam f ) = Lam (quote (i + 1) (f (vpar (Unquoted i))))
= case lookup x 0 of quote i (VNeutral n) = Inf (neutralQuote i n)
Just (HasType τ ) → return τ neutralQuote :: Int → Neutral → Term↑
Nothing → throwError "unknown identifier" neutralQuote i (NPar x) = varpar i x
type↑ i 0 (e1 :@: e2 ) neutralQuote i (NApp n v) = neutralQuote i n :@: quote i v
= do σ ← type↑ i 0 e1
case σ of Figure 7. Quotation in λ→
Fun τ τ 0 → do type↓ i 0 e2 τ
return τ 0
→ throwError "illegal application" corresponding substitution on the body. The type checker will never
type↓ :: Int → Context → Term↓ → Type → Result () encounter a bound variable; correspondingly the function type↑ has
type↓ i 0 (Inf e) τ no case for Var.
= do τ 0 ← type↑ i 0 e Note that the type equality check that is performed when check-
unless (τ == τ 0 ) (throwError "type mismatch") ing an inferable term is implemented by a straightforward syntactic
type↓ i 0 (Lam e) (Fun τ τ 0 ) equality on the data type Type. Our type checker does not perform
unification.
= type↓ (i + 1) ((Bound i, HasType τ ) : 0)
The code for substitution is shown in Figure 6, and again com-
(subst↓ 0 (Par (Bound i)) e) τ 0
prises a function for checkable (subst↓ ) and one for inferable terms
type↓ i 0
(subst↑ ). The integer argument indicates which variable is to be
= throwError "type mismatch" substituted. The interesting cases are the one for Var where we
check if the variable encountered is the one to be substituted or
Figure 5. Implementation of a type checker for λ→ not, and the case for Lam, where we increase i to reflect that the
variable to substitute is referenced by a higher number underneath
the binder.
Type checking We now implement the rules in Figure 3. The code Our implementation of the simply-typed lambda calculus is now
is shown in Figure 5. The type checking algorithm can fail, and almost complete. A small problem that remains is the evaluator
to do so gracefully, it returns a result in the Result monad. For returns a Value, and we currently have no way to print elements
simplicity, we choose a standard error monad in this presentation: of type Value.
type Result α = Either String α
Quotation As we mentioned earlier, the use of higher-order ab-
We use the function throwError :: String → Result α to report an stract syntax requires us to define a quote function that takes a
error. Value back to a term. As the VLam constructor of the Value data
The function for inferable terms type↑ returns a type, whereas type takes a function as argument, we cannot simply derive Show
the the function for checkable terms type↓ takes a type as input and Eq as we did for the other types. Therefore, as soon as we want
and returns ().The well-formedness of types is checked using the to get back at the internal structure of a value, for instance to dis-
function kind↓ . Each case of the definitions corresponds directly to play results of evaluation, we need the function quote. The code is
one of the rules. given in Figure 7.
The type-checking functions are parameterized by an integer The function quote takes an integer argument that counts the
argument indicating the number of binders we have encountered. number of binders we have traversed. Initially, quote is always
On the initial call, this argument is 0, therefore we provide type↑ 0 called with 0, so we wrap this call in the function quote0 .
as a wrapper. If the value is a lambda abstraction, we generate a fresh variable
We use this integer to simulate the type rules in the handling Unquoted i and apply the Haskell function f to this fresh variable.
of bound variables. In the type rule for lambda abstraction, we The value resulting from the function application is then quoted at
add the bound variable to the context while checking the body. level i + 1. We use the constructor Unquoted that takes an argument
We do the same in the implementation. The counter i indicates the of type Int here to ensure that the newly created names do not clash
number of binders we have passed, so Bound i is a fresh name that with other names in the value.
we can associate with the bound variable. We then add Bound i If the value is a neutral term (hence an application of a free
to the context 0 when checking the body. However, because we variable to other values), the function neutralQuote is used to quote
are turning a bound into a free variable, we have to perform the the arguments. The varpar function checks if the variable occurring

4 2007/6/18
at the head of the application is an Unquoted bound variable or a With assume, names are introduced and added to the context. For
constant: each term, the interpreter performs type checking and evaluation,
and shows the final value and the type.
varpar :: Int → Name → Term↑
varpar i (Unquoted k) = Var (i − k − 1)
varpar i x = Par x 3. Dependent types
Quotation of functions is best understood by example. The value In this section, we will modify the type system of the simply-
corresponding to the term const is VLam (λx → VLam (λy → x)). typed lambda calculus into a dependently-typed lambda calculus,
Applying quote0 yields the following: called λ5 . The differences are relatively small; in some cases,
introducing dependent types even simplifies our code. We begin
quote 0 (VLam (λx → VLam (λy → x))) by discussing the central ideas motivating the upcoming changes.
= Lam (quote 1 (VLam (λy → vpar (Unquoted 0))))
= Lam (Lam (quote 2 (vpar (Unquoted 0)))) Dependent function space In Haskell we can define polymorphic
= Lam (Lam (neutralQuote 2 (NPar (Unquoted 0)))) functions, such as the identity:
= Lam (Lam (Var 1))
id :: ∀α.α → α
When quote moves underneath a binder, we introduce a temporary id = λx → x
name for the bound variable. To ensure that names invented during By using polymorphism, we can avoid writing the same function
quotation do not interfere with any other names, we only use the on, say, integers and booleans. When such expressions are trans-
constructor Unquoted during the quotation process. If the bound lated to GHC’s core language, the polymorphism does not disap-
variable actually occurs in the body of the function, we will sooner pear. Instead, the identity function in the core takes two arguments:
or later arrive at those occurrences. We can then generate the correct a type α and a value of type α. Calls to the identity function in the
de Bruijn index by determining the number of binders we have core, must explicitly instantiate the identity function with a type:
passed between introducing and observing the Unquoted variable.
id Bool True :: Bool
Examples We can now test the implementation on our running id Int 3 :: Int
examples. We make the following definitions Haskell’s polymorphism allows types to abstract over types. Why
id0 = Lam (Inf (Var 0)) would you want to do anything different? Consider the following
const0 = Lam (Lam (Inf (Var 1))) data types:
tpar α = TPar (Const α) data Vector1 α = Vector1 α
par x = Inf (Par (Const x)) data Vector2 α = Vector2 α α
term1 = Ann id0 (Fun (tpar "a") (tpar "a")) :@: par "y" data Vector3 α = Vector3 α α α
term2 = Ann const0 (Fun (Fun (tpar "b") (tpar "b")) Clearly, there is a pattern here. We would like to write down a
(Fun (tpar "a") type with the following kind:
(Fun (tpar "b") (tpar "b"))))
:@: id0 :@: par "y" ∀α :: ∗.∀n :: Nat.Vec α n
env1 = [(Const "y", HasType (tpar "a")), but we cannot do this in Haskell. The problem is that the type Vec
(Const "a", HasKind Star)] abstracts over the value n.
env2 = [(Const "b", HasKind Star)] +
+ env1 The dependent function space ‘∀’ generalizes the usual function
space ‘→’ by allowing the range to depend on the domain. The
and then run an interactive session in Hugs or GHCi2 : parametric polymorphism known from Haskell can be seen as a
i quote0 (eval↑ term1 [ ]) special case of a dependent function, motivating our use of the
Inf (Par (Const "y")) symbol ‘∀’.4 In contrast to polymorphism, the dependent function
i quote0 (eval↑ term2 [ ]) space can abstract over more than just types. The Vec type above is
Lam (Inf (Var 0)) a valid dependent type.
i type↑ 0 env1 term1 It is important to note that the dependent function space is a
Right (TPar (Const "a"))
generalization of the usual function space. We can, for instance,
type the identity function on vectors as follows:
i type↑ 0 env2 term2
Right (Fun (TPar (Const "b")) (TPar (Const "b"))) ∀α :: ∗.∀n :: Nat.∀v :: Vec α n.Vec α n

We have implemented a parser, pretty-printer and a small read-eval- Note that the type v does not occur in the range: this is simply
the non-dependent function space already familiar to Haskell pro-
print loop,3 so that the above interaction can more conveniently
grammers. Rather than introduce unnecessary variables, such as v,
take place as:
we use the ordinary function arrow for the non-dependent case. The
ii assume (α :: ∗) (y :: α) identity on vectors then has the following, equivalent, type:
ii ((λx → x) :: α → α) y ∀α :: ∗.∀n :: Nat.Vec α n → Vec α n
y :: α
In Haskell, one can sometimes ‘fake’ the dependent function
ii assume β :: ∗
space [15], for instance by defining natural numbers on the type
ii ((λx y → x) :: (β → β) → α → β → β) (λx → x) y level (i.e., by defining data types Zero and Succ). Since the type
λx → x :: β → β level numbers are different from the value level natural numbers,
one then end up duplicating a lot of concepts on both levels. Fur-
2 Using lhs2T X [10], one can generate a valid Haskell program from the
E thermore, even though one can lift certain values to the type level
sources of this paper. The results given here automatically generated when in this fashion, additional effort – in the form of advanced type
this paper is typeset.
3 The code is included in the paper sources, but omitted from the typeset 4 Type theorists call dependent function types 5-types and would write
version for brevity. 5α : ∗.5n : Nat.Vec α n instead.

5 2007/6/18
e⇓v τ ⇓ v τ 0 ⇓ v0 0 ::= ε empty context valid(0) 0 ` τ ::↓ ∗
e :: τ ⇓ v ∗⇓∗ ∀x :: τ.τ 0 ⇓ ∀x :: v.v0 x⇓x | 0, x :: τ adding a variable valid(ε) valid(0, x :: τ )
e1 ⇓ λx → v1 e2 ⇓ v2 e1 ⇓ n1 e2 ⇓ v2 e⇓v
Figure 9. Contexts in λ5
e1 e2 ⇓ v1 [x 7→ v2 ] e1 e2 ⇓ n1 v2 λx → e ⇓ λx → v

Figure 8. Evaluation in λ5
0 ` τ ::↓ ∗ 0 ` e ::↓ τ 0 ` τ ::↓ ∗ 0, x :: τ ` τ 0 ::↓ ∗
class programming – is required to perform any computation on 0 ` (e :: τ ) ::↑ τ 0 ` ∗ ::↑ ∗ 0 ` ∀x :: τ.τ 0 ::↑ ∗
such types. Using dependent types, we can parameterize our types
by values, and as we will shortly see, the normal evaluation rules 0(x) = τ 0 ` e1 ::↑ ∀x :: τ.τ 0 0 ` e2 ::↓ τ
apply. 0 ` x ::↑ τ 0 ` e1 e2 ::↑ τ 0 [x 7→ e2 ]
Everything is a term Allowing values to appear freely in types
breaks the separation of expressions, types, and kinds we men- 0 ` e ::↑ τ 0 τ ⇓ v τ0 ⇓ v 0, x :: τ ` e ::↓ τ 0
tioned in the introduction. There is no longer a distinction between 0 ` e ::↓ τ 0 ` λx → e ::↓ ∀x :: τ.τ 0
these different levels: everything is a term. In Haskell, the symbol
‘::’ relates entities on different levels: In 0 :: Nat, the 0 is a value Figure 10. Type rules for λ5
and Nat a type, in Nat :: ∗, the Nat is a type and ∗ is a kind. Now,
∗, Nat and 0 are all terms. While 0 :: Nat and Nat :: ∗ still hold, the
symbol ‘::’ relates two terms. All these entities now reside on the
3.3 Type system
same syntactic level.
We have now familiarized ourselves with the core ideas of Before we approach the type rules themselves, we must take a look
dependently-typed systems. Next, we discuss what we have to at contexts again. It turns out that because everything is a term
change in λ→ in order to accomplish these ideas and arrive at λ5 . now, the syntax of contexts becomes simpler, as do the rules for
the validity of contexts (Figure 9, compare with Figure 2).
3.1 Abstract syntax Contexts now contain only one form of entry, stating the type a
We no longer have the need for a separate syntactic category of variable is assumed to have. The precondition 0 ` τ ::↓ ∗ in the
types or kinds, all constructs for all levels are now integrated into validity rule for non-empty contexts no longer refers to a special
the expression language: judgement for the well-formedness of types, but to the type rules
we are about to define – we no longer need special well-formedness
e, τ, κ ::= e :: τ annotated term rules for types. The precondition ensures in particular that τ does
| ∗ the type of types
not contain unknown variables.
| ∀x :: τ.τ 0 dependent function space The type rules are given in Figure 10. Again, we have high-
| x variable lighted the differences to Figure 3. We keep the difference between
| e1 e2 application
rules for inference (::↑ ), where the type is an output, and checking
| λx → e lambda abstraction
(::↓ ), where the type is an input. The new constructs ∗ and ∀ are
The modifications compared to the abstract syntax of the simply- among the constructs for which we can infer the type. As before
typed lambda calculus in Section 2.1 are highlighted. for λ→ , we assume that all the contexts that occur in the type rules
We now also use the symbols τ and κ to refer to expressions, are valid.
that is, we use them if the terms denoted play the role of types The only change for an annotated term is that – similar to what
or kinds, respectively. For instance, the occurrence of τ in an we have already seen for contexts – the kind check for τ no longer
annotated term now refers to another expression. refers to the well-formedness rules for types, but is an ordinary type
New constructs are imported from what was formerly in the check itself.
syntax of types and kinds. The kind ∗ is now an expression. Arrow The kind ∗ is itself of type ∗. Although there are theoretical
kinds and arrow types are subsumed by the new construct for the objections to this choice [6], we believe that for the purpose of this
dependent function space. Type variables and term variables now paper, the simplicity of our implementation outweighs any such
coincide. objections.
The rule for the dependent function space is somewhat similar
3.2 Evaluation to the well-formedness rule for arrow types for λ→ in Figure 2.
The modified evaluation rules are in Figure 8. All the rules are the Both the domain τ and the range τ 0 of the dependent function are
same as in the simply-typed case in Figure 1, only the rules for the required to be of kind ∗. In contrast to the rule in Figure 2, τ 0 may
two new constructs are added. Perhaps surprisingly, evaluation now refer to x, thus we extend 0 by x :: τ while checking e0 .
also extends to types. But this is exactly what we want: The power Nothing significant changes for variables.
of dependent types stems exactly from the fact that we can mix val- In a function application, the function must now be of a depen-
ues and types, and that we have functions, and thus computations dent function type ∀x :: τ.τ 0 . The difference to an ordinary function
on the type level. However, the new constructs are comparatively type is that τ 0 can refer to x. In the result type of the application, we
uninteresting for computation: the ∗ evaluates to itself; in a depen- therefore substitute the actual argument e2 for the formal parame-
dent function space, we recurse structurally, evaluating the domain ter x in τ 0 .
and the range. Therefore, we must extend the abstract syntax of Checking an inferable term works as before: we first infer a
values: type, then compare the two types for equality. However, types
v ::= x v name application are now terms and can contain computations, so syntactic equal-
| ∗ the type of types ity would be far too restrictive: it would be rather unfortunate if
| ∀x :: v.v0 dependent function space
Vec α 2 and Vec α (1 + 1) did not denote the same type. As a
| λx → v lambda abstraction result, we evaluate the types to normal forms and compare the nor-
mal forms syntactically. Most type systems with dependent types

6 2007/6/18
have a rule of the form: eval↑ Star d = VStar
0 ` e :: τ τ =β τ 0 eval↑ (Pi τ τ 0 ) d = VPi (eval↓ τ d) (λx → eval↓ τ 0 (x : d))
0 ` e :: τ 0
This rule, referred to as the conversion rule, however, is clearly not iSubst i r Star = Star
syntax directed. Our distinction between inferable and checkable iSubst i r (Pi τ τ 0 ) = Pi (cSubst i r τ ) (cSubst (i + 1) r τ 0 )
terms ensures that the only place where we need to apply the
quote i VStar = Inf Star
conversion rule, is when a term is explicitly annotated with its type.
quote i (VPi v f )
The final type rule is for checking a lambda abstraction. The
= Inf (Pi (quote i v) (quote (i + 1) (f (vpar (Unquoted i)))))
difference here is that the type is a dependent function type. Note
that the bound variable x may now not only occur in the body of the
function e. The extended context 0, x :: τ is therefore used both for Figure 11. Extending evaluation, substitution and quotation to λ5
type checking the function body and kind checking the the range τ 0 .
To summarize, all the modifications are motivated by the two
key concepts we have introduced in the beginning of Section 3: the type↑ :: Int → Context → Term↑ → Result Type
function space is generalized to the dependent function space; types type↑ i 0 (Ann e τ )
and kinds are also terms. = do type↓ i 0 τ VStar
let v = eval↓ τ [ ]
3.4 Implementation
type↓ i 0 e v
The type rules we have given are still syntax-directed and algorith- return v
mic, so no major changes to the implementation is required. How- type↑ i 0 Star
ever, one difference between the implementation and the rules is = return VStar
that during the implementation, we always evaluate types as soon
as they have been kind checked. This means that most of the oc- type↑ i 0 (Pi τ τ 0 )
currences of types (τ or τ 0 ) in the rules are actually values in the = do type↓ i 0 τ VStar
implementation. let v = eval↓ τ [ ]
In the following, we go through all aspects of the implementa- type↓ (i + 1) ((Bound i, v) : 0)
tion, but only look at the definitions that have to be modified. (subst↓ 0 (Par (Bound i)) τ 0 ) VStar
Abstract syntax The type Name remains unchanged. So does the return VStar
type Term↓ . We no longer require Type and Kind, but instead add type↑ i 0 (Par x)
two new constructors to Term↑ and replace the occurrence of Type = case lookup x 0 of
in Ann with a Term↓ : Just v → return v
data Term↑ Nothing → throwError "unknown identifier"
= Ann Term↓ Term↓ type↑ i 0 (e1 :@: e2 )
| Star = do σ ← type↑ i 0 e1
| Pi Term↓ Term↓ case σ of
| Var Int VPi v f → do type↓ i 0 e2 v
| Par Name return (f (eval↓ e2 [ ]))
| Term↑ :@: Term↓ → throwError "illegal application"
deriving (Show, Eq)
type↓ :: Int → Context → Term↓ → Type → Result ()
We also extend the type of values with the new constructs.
type↓ i 0 (Inf e) v
data Value = do v0 ← type↑ i 0 e
= VLam (Value → Value)
unless (quote0 v == quote0 v0 ) (throwError "type mismatch")
| VStar
type↓ i 0 (Lam e) (VPi v f )
| VPi Value (Value → Value)
| VNeutral Neutral = type↓ (i + 1) ( (Bound i, v) : 0)
(subst↓ 0 (Par (Bound i)) e) (f (vpar (Bound i)))
As before, we use higher-order abstract syntax for the values,
type↓ i 0
i.e., we encode binding constructs using Haskell functions. With
= throwError "type mismatch"
VPi, we now have a new binding construct. In the dependent func-
tion space, a variable is bound that is visible in the range, but not in
the domain. Therefore, the domain is simply a Value, but the range Figure 12. Implementation of a type checker for λ5
is represented as a function of type Value → Value.
Evaluation To adapt the evaluator, we just add the two new cases Type checking Let us go through each of the cases in Figure 12
for Star and Pi to the eval↑ function, as shown in Figure 11 (see one by one. The cases for λ→ – for comparison – are in Figure 5.
Figure 4 for the evaluator for λ→ ). Evaluation of Star is trivial. For an annotated term, we first check that the annotation is a type
For a Pi type, both the domain and the range are evaluated. In the of kind ∗, using the type-checking function type↓ . We then evaluate
range, where the bound variable x is visible, we have to add it to the type. The resulting value v is used to check the term e. If that
the environment. succeeds, the entire expression has type v.
The (evaluated) type of Star is VStar.
Contexts Contexts map variables to their types. Types are on the
For a dependent function type, we first kind-check the domain τ .
term level now. We store types in their evaluated form, and thus
Then the domain is evaluated to v. The value is added to the context
define:
while kind-checking the range – the idea is similar to the type-
type Type = Value checking rules for Lam in λ→ and λ5 .
type Context = [(Name, Type )] There are no significant changes in the Par case.

7 2007/6/18
In the application case, the type inferred for the function is a k⇓l
Value now. This type must be of the form VPi v f , i.e., a dependent Nat ⇓ Nat Zero ⇓ Zero Succ k ⇓ Succ l
function type. In the corresponding type rule in Figure 10, the pz ⇓ v ps k (natElim m mz ms k) ⇓ v
bound variable x is substituted by e2 in the result type τ 0 . In the
natElim m mz ms Zero ⇓ v natElim m mz ms (Succ k) ⇓ v
implementation, f is the function corresponding to τ 0 , and the
substitution is performed by applying it to the (evaluated) e2 . Figure 13. Evaluation of natural numbers
In the case for Inf, we have to perform the type equality check.
In contrast to the type rules, we already have two values v and v0 .
To compare the values, we quote them and compare the resulting 0 ` k :: Nat
terms syntactically. 0 ` Nat :: ∗ 0 ` Zero :: Nat 0 ` Succ k :: Nat
In the case for Lam, we require a dependent function type of 0 ` m :: Nat → ∗
form VPi v f now. As in the corresponding case for λ→ , we add the 0, m :: Nat → ∗ ` mz :: m Zero
bound variable (of type v) to the context while checking the body. 0, m :: Nat → ∗ ` ms :: ∀k :: Nat.m k → m (Succ k)
But we now perform substitution on the function body e (using 0 ` n :: Nat
subst↓ ) and on the result type f (by applying f ). 0 ` natElim m mz ms n :: m n
We thus only have to extend the substitution functions, by
adding the usual two cases for Star and Pi as shown in Figure 11. Figure 14. Typing rules for natural numbers
There’s nothing to subsitute for Star. For Pi, we have to incre-
ment the counter before substituting in the range because we pass plus :: Nat → Nat → Nat
a binder. plus Zero n =n
plus (Succ k) n = Succ (plus k n)
Quotation To complete our implementation of λ5 , we only have
to extend the quotation function. This operation is more important In our calculus so far, we can neither pattern match nor make
than for λ→ , because as we have seen, it is used in the equality recursive calls. How could we hope to define plus?
check during type checking. Again, we only have to add equations In Haskell, we can define recursive functions on data types using
for VStar and VPi, which are shown in Figure 11. a fold [18]. Rather than introduce pattern matching and recursion,
Quoting VStar yields Star. Since the dependent function type is and all the associated problems, we define functions over natural
a binding construct, quotation for VPi works similar to quotation numbers using the corresponding fold. In a dependently type set-
of VLam: to quote the range, we increment the counter i, and apply ting, however, we can define a slightly more general version of a
the Haskell function representing the range to Unquoted i. fold called the eliminator.
The eliminator is a higher-order function, similar to a fold,
3.5 Where are the dependent types? describing how to write programs over natural numbers. The fold
for natural numbers has the following type:
We now have adapted our type system and its implementation
to dependent types, but unfortunately, we have not yet seen any foldNat :: ∀α :: ∗.α → (α → α) → Nat → α
examples. This much should be familiar. In the context of dependent types,
Again, we have written a small interpreter around the type however, we can be more general. There is no need for the type α
checker we have just presented, and we can use it to define and to be uniform across the constructors for natural numbers: rather
check, for instance, the polymorphic identity function (where the than use α :: ∗, we use m :: Nat → ∗. This leads us to the following
type argument is explicit), as follows: type of natElim:
ii let id = (λα x → x) :: ∀(α :: ∗).α → α natElim :: ∀m :: Nat → ∗. m Zero
id :: ∀x :: ∗.∀y :: x.x → (∀k :: Nat.m k → m (Succ k))
ii assume (Bool :: ∗) (False :: Bool) → ∀n :: Nat.m n
ii id Bool The first argument of the eliminator is the sometimes referred to as
λx → x :: ∀x :: Bool.Bool the motive [14]; it explains the reason we want to eliminate natural
ii id Bool False numbers. The second argument corresponds to the base case, where
False :: Bool n is Zero; the third argument corresponds to the inductive case
This is more than we can do in the simply-typed setting, but it where n is Succ k, for some k. In the inductive case, we must
is by no means spectacular and does not require dependent types. describe how to construct m (Succ k) from k and m k. The result of
Unfortunately, while we have a framework for dependent types in natElim is a function that given any natural number n, will compute
place, we cannot write any interesting programs as long as we do a value of type m n.
not add at least a few specific data types to our language. In summary, adding natural numbers to our language involves
adding three separate elements: the type Nat, the constructors Zero
and Succ, and the eliminator natElim.
4. Beyond λ5
In Haskell, data types are introduced by special data declarations: 4.1 Implementing natural numbers

data Nat = Zero | Succ Nat


To implement these three components, we extend the abstract syn-
tax and correspondingly add new cases to the evaluation and type
This introduces a new type Nat, together with two constructors checking functions. These new cases do not require any changes to
Zero and Succ. In this section, we investigate how to extend our existing code; we choose to focus only on the new code fragments.
language with data types, such as natural numbers.
Abstract Syntax To implement natural numbers, we extend our
Obviously, we will need to add the type Nat together with its
abstract syntax as follows:
constructors; but how should we define functions, such as addition,
that manipulate numbers? In Haskell, we would define a function data Term↑ = . . .
that pattern matches on its arguments and makes recursive calls to | Nat
smaller numbers: | NatElim Term↓ Term↓ Term↓ Term↓

8 2007/6/18
eval↓ Zero d = VZero type↓ i 0 Zero VNat = return ()
eval↓ (Succ k) d = VSucc (eval↓ k d) type↓ i 0 (Succ k) VNat = type↓ i 0 k VNat
eval↑ Nat d = VNat type↑ i 0 Nat = return VStar
eval↑ (NatElim m mz ms n) d type↑ i 0 (NatElim m mz ms n) =
= let mzVal = eval↓ mz d do type↓ i 0 m (VPi VNat (const VStar))
msVal = eval↓ ms d let mVal = eval↓ m [ ]
rec nVal = type↓ i 0 mz (mVal ‘vapp‘ VZero)
case nVal of type↓ i 0 ms
VZero → mzVal (VPi VNat (λn →
VSucc k → msVal ‘vapp‘ k ‘vapp‘ rec k VPi (mVal ‘vapp‘ n) (λ →
VNeutral n → VNeutral mVal ‘vapp‘ VSucc n)))
(NNatElim (eval↓ m d) mzVal msVal n) type↓ i 0 n VNat
→ error "internal: eval natElim" let nVal = eval↓ n [ ]
in rec (eval↓ n d) return (mVal ‘vapp‘ nVal)

Figure 15. Extending the evaluator natural numbers Figure 16. Extending the type checker for natural numbers

data Term↓ = . . . natElim :: ∀m :: Nat → ∗. m Zero


| Zero → (∀k :: Nat.m k → m (Succ k))
| Succ Term↓ → ∀n :: Nat.m n
We add new constructors corresponding to the type of and elim- We begin by type checking and evaluating the motive m. Once we
inator for natural numbers to the Term↑ data type. The NatElim have the value of m, we type check the two branches. The branch
constructor is fully applied: it expects no further arguments. for zero should have type m Zero; the branch for successors should
Similarly, we extend Term↓ with the constructors for natural have type ∀k :: Nat.m k → m (Succ k). Despite the apparent
numbers. This may seem odd: we will always know the type of complication resulting from having to hand code complex types,
Zero and Succ, so why not add them to Term↑ instead? For more type checking these branches is exactly what would happen when
complicated types, however, such as dependent pairs, it is not type checking a fold over natural numbers in Haskell. Finally, we
always possible to infer the type of the constructor without a type check that the n we are eliminating is actually a natural number.
annotation. We choose to add all constructors to Term↓ , as this The return type of the entire expression is the motive, accordingly
scheme will work for all data types. applied to the number being eliminated.
Evaluation We need to rethink our data type for values. Pre- Other functions To complete the implementation of natural num-
viously, values consisted exclusively of lambda abstractions and bers, we must also extend the auxiliary functions for substitution
‘stuck’ applications. Clearly, we will need to extend the data type and quotations with new cases. All new code is, however, com-
for values to cope with the new constructors for natural numbers. pletely straight-forward, because no new binding constructs are in-
data Value = . . .
volved.
| VNat Addition With all the ingredients in place, we can finally define
| VZero addition in our interpreter as follows:
| VSucc Value
ii let plus = natElim (λ → Nat → Nat)
Introducing the eliminator, however, also complicates evaluation. (λn → n)
The eliminator for natural numbers can also be stuck when the (λk rec n → Succ (rec n))
number being eliminated does not evaluate to a constructor. Cor- plus :: ∀x :: Nat.∀y :: Nat.Nat
respondingly, we extend the data type for neutral terms covers this We define a function plus by eliminating the first argument of the
case: addition. In each case branch, we must define a function of type
data Neutral = . . . Nat → Nat; we choose our motive correspondingly. In the base
| NNatElim Value Value Value Neutral case, we must add zero to the argument n – we simply return n. In
the inductive case, we are passed the predecessor k, the recursive
The implementation of evaluation in Figure 15 closely follows call rec (that corresponds to adding k), and the number n, to which
the rules in Figure 13. The eliminator is the only interesting case. we must add Succ k. We proceed by adding k to n using rec, and
Essentially, the eliminator evaluates to the Haskell function with the wrapping an additional Succ around the result. After having defined
behaviour you would expect: if the number being eliminated evalu-
plus, we can evaluate simple additions in our interpreter5 :
ates to VZero, we evaluate the base case mz; if the number evaluates
to VSucc k, we apply the step function ms to the predecessor k and ii plus 40 2
the recursive call to the eliminator; finally, if the number evaluates 42 :: Nat
to a neutral term, the entire expression evaluates to a neutral term. Clearly programming with eliminators does not scale very well.
If the value being eliminated is not a natural number or a neutral We defer the discussion about how this might be fixed to Section 5.
term, this would have already resulted in a type error. Therefore,
the final catch-all case should never be executed. 4.2 Implementing vectors
Natural numbers are still not particularly exciting: they are still
Typing Figure 16 contains the implementation of the type checker
the kind of data type we can write quite easily in Haskell. As an
that deals with natural numbers. Checking that Zero and Succ con-
example of a data type that really makes use of dependent types,
struct natural numbers is straightforward.
we show how to implement vectors.
Type checking the eliminator is bit more involved. Remember
that the eliminator has the following type: 5 For convenience, our parser and pretty-printer support literals for natural
numbers. For instance, 2 is translated to Succ (Succ Zero)) :: Nat on the fly.

9 2007/6/18
eval↑ (VecElim α m mn mc n xs) d = type↓ i 0 (Nil α) (VVec bVal VZero) =
let mnVal = eval↓ mn d do type↓ i 0 α VStar
mcVal = eval↓ mc d let aVal = eval↓ α [ ]
rec nVal xsVal = unless (quote0 aVal == quote0 bVal)
case xsVal of (throwError "type mismatch")
VNil → mnVal type↓ i 0 (Cons α n x xs) (VVec bVal (VSucc k)) =
VCons k x xs → foldl vapp mcVal [k, x, xs, rec k xs] do type↓ i 0 α VStar
VNeutral n → VNeutral let aVal = eval↓ α [ ]
(NVecElim (eval↓ α d) (eval↓ m d) unless (quote0 aVal == quote0 bVal)
mnVal mcVal nVal n) (throwError "type mismatch")
→ error "internal: eval vecElim" type↓ i 0 n VNat
in rec (eval↓ n d) (eval↓ xs d) let nVal = eval↓ n [ ]
unless (quote0 nVal == quote0 k)
Figure 17. Implementation of the evaluation of vectors (throwError "number mismatch")
type↓ i 0 x aVal
As was the case for natural numbers, we need to define three type↓ i 0 xs (VVec bVal k)
separate components: the type of vectors, its the constructors, and type↑ i 0 (Vec α n) =
the eliminator. We have already mentioned that vectors are param- do type↓ i 0 α VStar
eterized by both a type and a natural number: type↓ i 0 n VNat
∀α :: ∗.∀n :: Nat.Vec α n :: ∗ return VStar
The constructors for vectors are analogous to those for Haskell type↑ i 0 (VecElim α m mn mc n vs) =
lists. The only difference is that their types record the length of do type↓ i 0 α VStar
the vector: let aVal = eval↓ α [ ]
Nil :: ∀α :: ∗.Vec α Zero type↓ i 0 m
Cons :: ∀α :: ∗.∀n :: Nat.α → Vec α n → Vec α (Succ n) (VPi VNat (λn →
VPi (VVec aVal n) (λ →
The eliminator for vectors behaves essentially the same as foldr VStar)))
on lists, but its type is a great deal more specific (and thus, more let mVal = eval↓ m [ ]
involved):
type↓ i 0 mn (foldl vapp mVal [VZero, VNil aVal])
vecElim :: ∀α :: ∗.∀m :: (∀n :: Nat.Vec α n → ∗). type↓ i 0 mc
m Zero (Nil α) (VPi VNat (λn →
→ (∀n :: Nat.∀x :: α.∀xs :: Vec α n. VPi aVal (λy →
m n xs → m (Succ n) (Cons α n x xs)) VPi (VVec aVal n) (λys →
→ ∀n :: Nat.∀xs :: Vec α n.m n xs
VPi (foldl vapp mVal [n, ys]) (λ →
The whole eliminator is quantified over the element type α of the (foldl vapp mVal [VSucc n, VCons aVal n y ys]))))))
vectors. The next argument of the eliminator is the motive. As was type↓ i 0 n VNat
the case for natural numbers, the motive is a type (kind ∗) param- let nVal = eval↓ n [ ]
eterized by a vector. As vectors are themselves parameterized by type↓ i 0 vs (VVec aVal nVal)
their length, the motive expects an additional argument of type Nat. let vsVal = eval↓ vs [ ]
The following two arguments are the cases for the two constructors return (foldl vapp mVal [nVal, vsVal])
of Vec. The constructor Nil is for empty vectors, so the correspond-
ing argument is of type m Zero (Nil α). The case for Cons takes Figure 18. Extending the type checker for vectors
a number n, a element x of type α, a vector xs of length n, and the
result of the recursive application of the eliminator of type m n xs.
It combines those elements to form the required type, for the vector data Term↓ = . . .
of length Succ n where x has been added to xs. The final result is a | Nil Term↓
function that eliminates a vector of any length. | Cons Term↓ Term↓ Term↓ Term↓
The type of the eliminator may look rather complicated. How- Note that also Nil takes an argument, because both constructors are
ever, if we compare with the type of foldr on lists polymorphic in the element type. Correspondingly, we extend the
foldr :: ∀α :: ∗.∀m :: ∗.m → (α → m → m) → [α ] → m data types for values and neutral terms:
we see that the structure is the same, and the additional complexity data Value = . . .
stems only from the fact that the motive is parameterized by a | VNil Value
vector, and vectors are in turn parameterized by natural numbers. | VCons Value Value Value Value
Not all of the arguments of vecElim are actually required – some | VVec Value Value
of the arguments can be inferred from others, to reduce the noise
data Neutral = . . .
and make writing programs more feasible. We would like to remind | NVecElim Value Value Value Value Value Neutral
you that λ5 is designed to be a very explicit, low-level language.
Abstract syntax As was the case for natural numbers, we extend Evaluation Evaluation of constructors or the Vec type proceeds
the abstract syntax. We add the type of vectors and its eliminator to structurally, turning terms into their value counterparts. Once again,
Term↑ ; we extend Term↓ with the constructors Nil and Cons. the only interesting case is the evaluation of the eliminator for
vectors, shown in Figure 17. As indicated before, the behaviour
data Term↑ = . . .
resembles a fold on lists: depending on whether the vector is a
| Vec Term↓ Term↓
VNil or a VCons, we apply the appropriate argument. In the case
| VecElim Term↓ Term↓ Term↓ Term↓ Term↓ Term↓

10 2007/6/18
for VCons, we also call the eliminator recursively on the tail of the work is, in fact, still subject to exciting new research. There is really
vector (of length k). If the eliminated vector is a neutral element, too much to cover, but we attempt to identify several important
we cannot reduce the eliminator, and produce a neutral term again. points.
Type checking We extend the type checker as shown in Figure 18. Program development As it stands, the core system we have pre-
The code is relatively long, but keeping the types of each of the sented requires programmers to explicitly instantiate polymorphic
constructs in mind, there are absolutely no surprises. functions. This is terribly tedious! Take the append function we de-
As for natural numbers, we have omitted the new cases for sub- fined: of its five arguments, only two are interesting. Fortunately,
stitution and quotation, because they are entirely straight-forward. uninteresting arguments can usually be inferred. Many program-
Append We are now capable of demonstrating a real dependently ming languages and proof assistants based on dependent types have
typed program in action, a function that appends two vectors while support for implicit arguments that the user can omit when calling a
keeping track of their lengths. The definition in the interpreter looks function. Note that these arguments need not be types: the append
as follows: function is ‘polymorphic’ in the length of the vectors.
Writing programs with complex types in one go is not easy.
ii let append = Epigram [9] and Agda [20] allow programmers to put ‘holes’ in
(λα → vecElim α their code, leaving parts of their programs undefined. Programmers
(λm → ∀(n :: Nat).Vec α n → Vec α (plus m n)) can then ask the system what type a specific hole has, effectively
(λ v → v) allowing the incremental development of complex programs.
(λm v vs rec n w → Cons α (plus m n) v (rec n w))) Our system cannot do any type inference. The distinction be-
:: ∀(α :: ∗) (m :: Nat) (v :: Vec α m) (n :: Nat) (w :: Vec α n).
tween Term↑ s and Term↓ s may minimize the number of annota-
Vec α (plus m n)
tions that must be present, but types are never inferred. This may
Like for plus, we define a binary function on vectors by eliminating seem quite a step back from Haskell. On the contrary, once you
the first argument. The motive is chosen to expect a second vector. provide such detailed type information parts of your program can
The length of the resulting vector is the sum of the lengths of the actually be inferred. This is demonstrated by Epigram. In Epigram
argument vectors plus m n. Appending an empty vector to another all high-level programs are ‘compiled’ down to a core type theory,
vector v results in v. Appending a vector of the form Cons m v vs to similar to the one we have defined hered. When you perform case
a vector v works by invoking recursion via rec (which appends vs analysis on a non-empty vector, however, you do not need to write
to w) and prepending v. Of course, we can also apply the function the case for Nil. In reality, a clever choice of motive and automation
thus defined: explains to the eliminator why that branch is impossible. While we
ii assume (α :: ∗) (x :: α) (y :: α) may need to be more explicit about our types, the type information
can help guide our program development.
ii append α 2 (Cons α 1 x (Cons α 0 x (Nil α)))
1 (Cons α 0 y (Nil α)) Totality As our implementation illustrates, typing checking a de-
Cons α 2 x (Cons α 1 x (Cons α 0 y (Nil α))) :: Vec α 3 pendently typed programming language involves evaluating func-
We assume a type α with two elements x and y, and append a vector tions statically. For this reason, it is important to know when a func-
containing two x’s to a vector containing one y. tion is total, i.e., when a function is guaranteed to produce a result
for every in input in finite time. If we guarantee that only total func-
4.3 Summary tions may be evaluated during type checking, type checking will
In this section, we have demonstrated how to add two data types remain decidable.
to the λ5 : natural numbers and vectors. Using exactly the same On the other hand, most functional programmers write partial
principles, many more data types can be added. For example, for functions all the time. For example, the head function only works
any natural number n, we can define the type Fin n that contains on non-empty lists. Similarly, functions may use general recursion
exactly n elements. In particular, Fin 0, Fin 1 and Fin 2 are the and diverge on malformed input. In a sense the situation is akin to
empty type, the unit type, and the type of booleans respectively. Haskell, before the introduction of monads to encapsulate IO. It is
Furthermore, Fin can be used to define a total projection function clear that dependently typed programming languages must come to
from vectors, of type project :: ∀(α :: ∗) (n :: Nat).Vec α n → terms with partial functions. Finding the right solution, however, is
Fin n → α. still very much an open problem.
Another interesting dependent type is the equality type Eq :: Cayenne [1], for instance, allowed programmers to freely de-
∀(α :: ∗).α → α → ∗ with a single constructor Refl :: ∀(α :: fine functions using general recursion. As a result, Cayenne’s
∗) (x :: α) → Eq α x x, Using Eq, we can state and prove type checker could loop. Similar problems arise when we allow
theorems about our code directly in λ5 . For instance, the type Haskell’s case construct: the type checker could crash with an ex-
∀(α :: ∗) (n :: Nat).Eq Nat (plus n Zero) n states that Zero is ception equivalent to Prelude.head: empty list. In this paper
the right-neutral element of addition. Any term of that type serves we taken a more prudent approach and only allow recursion and
as a proof of that theorem, via the Curry-Howard isomorphism. pattern matching via eliminators, which are guaranteed to produce
A few of these examples are included with the interpreter in total functions.
the paper sources, which can be downloaded via the λ5 home- As our examples illustrate, however, programming with elim-
page [11]. More about suitable data types for dependently typed inators does not scale. Epigram uses a clever choice of motive
languages and writing dependently-typed programs can be found to make programming with eliminators a great deal more practi-
in another tutorial [16]. cal [13, 17]. By choosing the right motive, we can exploit type in-
formation when defining complicated functions. Eliminators may
not appear to be terribly useful, but they form the foundations on
5. Toward dependently-typed programming which dependently typed programming languages may be built.
The calculus we have described is far from a real programming There may be situations, however, where programming with
language. Although we can write, type check, and evaluate simple eliminators is just not enough. One possible solution is to permit
expressions there is still a lot of work to be done before it becomes general recursion, encapsulated in a suitable monad. Just as the
feasible to write large, complex programs. Most of the remaining IO monad encapsulates impure functions in Haskell, monads can

11 2007/6/18
also be used to introduce partial functions into a total language [5]. you through your first steps, but encourage you to start exploring
Finding the right way to tackle partiality, without sacrificing theo- dependent types yourself!
retical properties of the type system, requires both more research
and practical experience. References
[1] Lennart Augustsson. Cayenne – a language with dependent types.
Real world programming Although we have added several data In ICFP ’98: Proceedings of the Third ACM SIGPLAN International
types to the core theory, we cannot expect programmers to imple- Conference on Functional Programming, pages 239–250, 1998.
ment new data types by hacking them into the compiler. We need
[2] Yves Bertot and Pierre Castéran. Interactive Theorem Proving
to add support for user-defined data types. Once again, we must be
and Program Development. Coq’Art: The Calculus of Inductive
careful about which data types we allow. For example, general re- Constructions. Texts in Theoretical Computer Science. Springer
cursion can be introduced via data types with negative occurrences, Verlag, 2004.
such as our Value type.
[3] Edwin Brady. Practical Implementation of a Dependently Typed
Recently, generalized algebraic data types have caused quite
Functional Programming Language. PhD thesis, Durham University,
some excitement in the Haskell community. In the presence of 2005.
dependent types, GADTs (or indexed families as they are known in
the type theory community) become even more expressive. Vectors [4] Edwin Brady, Conor McBride, and James McKinna. Inductive
families need not store their indices. In Types for Proofs and
already illustrate that indexing by a value, instead of a type, allows
Programs, volume 3085 of LNCS. Springer-Verlag, 2004.
you to be more precise about the structure of data and the invariants
it satisfies. This pattern pops up everywhere: well-typed lambda [5] Venanzio Capretta. General Recursion via Coinductive Types.
terms; proof-carrying code; red-black trees; the list goes on and on. Logical Methods in Computer Science, 1(2):1–18, 2005.
There has been very little research on how to compile depen- [6] T. Coquand. An analysis of Girard’s paradox. In First IEEE
dently typed languages. As a result, many people believe dependent Symposium on Logic in Computer Science, 1986.
types are inherently inefficient: naively compiling append would [7] Thierry Coquand. An algorithm for type-checking dependent types.
result in code that computes the length of the resulting vector, even Science of Computer Programming, 26(1-3):167–177, 1996.
if this information is not used anywhere. Such computations, how- [8] Thierry Coquand and Makoto Takeyama. An implementation of
ever, are only relevant during type checking. This illustrates how Type: Type. In International Workshop on Types for Proofs and
there is still a distinction between evaluation for the sake of type Programs, 2000.
checking and evaluation to compute the result of your program.
[9] Conor McBride et al. Epigram, 2004. http://www.e-pig.org.
Edwin Brady covers this, together with various optimizations only
possible due to the presence of richer type information, in his the- [10] Ralf Hinze and Andres Löh. lhs2TEX, 2007. http://www.iai.
sis [3, 4]. uni-bonn.de/∼loeh/lhs2tex.
[11] λ5 homepage, 2007. http://www.iai.uni-bonn.de/∼loeh/
LambdaPi.
6. Discussion [12] Per Martin-Löf. Intuitionistic type theory. Bibliopolis, 1984.
There is a large amount of relevant literature regarding both imple- [13] Conor McBride. Dependently Typed Functional Programs and their
menting type systems and type theory. Pierce’s book [21] is an ex- Proofs. PhD thesis, University of Edinburgh, 1999.
cellent place to start. Martin-Löf’s notes on type theory [12] are still [14] Conor McBride. Elimination with a motive. In TYPES ’00: Selected
highly relevant and form an excellent introduction to the subject. papers from the International Workshop on Types for Proofs and
More recent books by Nordström et al. [19] and Thompson [22] Programs, pages 197–216. Springer-Verlag, 2000.
are freely available online. [15] Conor McBride. Faking it: Simulating dependent types in Haskell.
There are several dependently typed programming languages Journal of Functional Programming, 12(5):375–392, 2002.
and proof assistants readily available. Coq [2] is a mature, well-
[16] Conor McBride. Epigram: Practical programming with dependent
documented proof assistant. While it is not primarily designed for types. In Advanced Functional Programming, pages 130–170, 2004.
dependently typed programming, learning Coq can help get a feel
for type theory. Haskell programmers may feel more at home using [17] Conor McBride and James McKinna. The view from the left. Journal
recent versions of Agda [20], a dependently typed programming of Functional Programming, 14(1):69–111, 2004.
language. Not only does the syntax resemble Haskell, but func- [18] Erik Meijer, Maarten Fokkinga, and Ross Paterson. Functional
tions may be defined using pattern matching and general recursion. programming with bananas, lenses, envelopes and barbed wire. In
Finally, Epigram [9, 16] proposes a more radical break from func- 5th Conf. on Functional Programming Languages and Computer
tional programming as we know it. While the initial implementa- Architecture, 1991.
tion is far from perfect, many of Epigram’s ideas are not yet imple- [19] Bengt Nordström, Kent Petersson, and Jan M. Smith. Programming
mented elsewhere. in Martin-Löf’s Type Theory: An Introduction. Clarendon, 1990.
Other implementations of the type system we have presented [20] Ulf Norell. Agda 2. http://appserv.cs.chalmers.se/users/
here have been published elsewhere [7, 8]. These implementations ulfn/wiki/agda.php.
are given in pseudocode and accompanied by a proof of correct- [21] Benjamin C. Pierce. Types and programming languages. MIT Press,
ness. The focus of our paper is somewhat different: we have chosen Cambridge, MA, USA, 2002.
to describe a concrete implementation as a vehicle for explanation.
[22] Simon Thompson. Type theory and functional programming. Addison
In the introduction we mentioned some of the concerns Haskell Wesley Longman Publishing Co., Inc., 1991.
programmers have regarding dependent types. The type checking
algorithm we have presented here is decidable and will always ter-
minate. The phase distinction between evaluation and type check-
ing becomes more subtle, but is not lost. The fusion of types and
terms introduces new challenges, but also has a lot to offer. Most
importantly, though, getting started with dependent types is not as
hard as you may think. We hope to have whet your appetite, guiding

12 2007/6/18

You might also like