---
title: Dependent Types
date: September 08, 2017
maths: true
---

Dependent types are pretty cool, yo. This post is a semi-structured
ramble about [dtt](https://ahti-saarelainen.zgrep.org/git/hydraz/dtt),
a small dependently-typed "programming language" inspired by Thierry
Coquand's Calculus of (inductive) Constructions (though, note that the
_induction_ part is still lacking: There is support for defining
inductive data types, and destructuring them by pattern matching, but
since there's no totality checker, recursion is disallowed).

`dtt` is written in Haskell, and served as a learning experience both in
type theory and in writing programs using [extensible
effects](https://hackage.haskell.org/package/freer). I *do* partly regret
the implementation of effects I chose (the more popular
[`extensible-effects`](https://hackage.haskell.org/package/extensible-effects)
did not build on the Nixpkgs channel I had, so I went with `freer`;
Refactoring between these should be easy enough, but I still haven't
gotten around to it, yet)

I originally intended for this post to be a Literate Haskell file,
interleaving explanation with code. However, for a pet project, `dtt`'s
code base quickly spiralled out of control, and is now over a thousand
lines long: It's safe to say I did not expect this one bit.

### The language

`dtt` is a very standard $\lambda_{\prod{}}$ calculus. We have all 4 axes of
Barendgret's lambda cube, in virtue of having types be first class
values: Values depending on values (functions), values depending on
types (polymorphism), types depending on types (type operators), and
types depending on values (dependent types). This places dtt squarely at
the top, along with other type theories such as the Calculus of
Constructions (the theoretical basis for the Coq proof assistant) and TT
(the type theory behind the Idris programming language).

The syntax is very simple. We have the standard lambda calculus
constructs - $\lambda$-abstraction, application and variables - along
with `let`{.haskell}-bindings, pattern matching `case` expression, and
the dependent type goodies: $\prod$-abstraction and `Set`{.haskell}.

_As an aside_, pi types are called as so because the dependent function
space may (if you follow the "types are sets of values" line of
thinking) be viewed as the cartesian product of types. Consider a type
`A`{.haskell} with inhabitants `Foo`{.haskell}, `Bar`{.haskell} and
a type `B`{.haskell} with inhabitant `Quux`{.haskell}. A dependent
product $\displaystyle\prod_{(x: \mathtt{A})}\mathtt{B}$, then, has
inhabitants `(Foo, Quux)`{.haskell} and `(Bar, Quux)`{.haskell}.

You'll notice that dtt does not have a dedicated arrow type. Indeed, the
dependent product subsumes both the $\forall$ quantifier of System $F$,
and the arrow type $\to$ of the simply-typed lambda calculus. Keep this
in mind: It'll be important later.

Since dtt's syntax is unified (i.e., there's no stratification of terms
and types), the language can be - and is - entirely contained in
a single algebraic data type. All binders are _explicitly typed_, seeing
as inference for dependent types is undecidable (and, therefore,
bad).[^1]

```haskell
type Type = Term
data Term
  = Variable Var
  | Set Int
  | TypeHint Term Type
  | Pi Var Type Type
  | Lam Var Type Term
  | Let Var Term Term
  | App Term Term
  | Match Term [(Pattern, Term)]
  deriving (Eq, Show, Ord)
```

The `TypeHint`{.haskell} term constructor, not mentioned before, is
merely a convenience: It allows the programmer to check their
assumptions and help the type checker by supplying a type (Note that we
don't assume this type is correct, as you'll see later; It merely helps
guide inference.)

Variables aren't merely strings because of the large amount of
substitutions we have to perform: For this, instead of generating a new
name, we increment a counter attached to the variable - the pretty
printer uses the original name to great effect, when unambiguous.

```haskell
data Var
  = Name String
  | Refresh String Int
  | Irrelevant
  deriving (Eq, Show, Ord)
```

The `Irrelevant`{.haskell} variable constructor is used to support $a
\to b$ as sugar for $\displaystyle\prod_{(x: a)} b$ when $x$ does not
appear free in $b$. As soon as the type checker encounters an
`Irrelevant`{.haskell} variable, it is refreshed with a new name.

`dtt` does not have implicit support (as in Idris), so all parameters,
including type parameters, must be bound explicitly. For this, we
support several kinds of syntatic sugar. First, all abstractions support
multiple variables in a _binding group_. This allows the programmer to
write `(a, b, c : α) -> β` instead of `(a : α) -> (b : α) -> (c : α) ->
β`. Furthermore, there is special syntax `/\a` for single-parameter
abstraction with type `Set 0`{.haskell}, and lambda abstractions support
multiple binding groups.

As mentioned before, the language does not support recursion (either
general or well-founded). Though I would like to, writing a totality
checker is hard - way harder than type checking $\lambda_{\prod{}}$, in
fact. However, an alternative way of inspecting inductive values _does_
exist: eliminators. These are dependent versions of catamorphisms, and
basically encode a proof by induction. An inductive data type as Nat
gives rise to an eliminator much like it gives rise to a natural
catamorphism.

```
inductive Nat : Type of {
  Z : Nat;
  S : Nat -> Nat
}

natElim : (P : Nat -> Type)
       -> P Z
       -> ((k : Nat) -> P k -> P (S k))
       -> (n : Nat)
       -> P n
```

If you squint, you'll see that the eliminator models a proof by
induction (of the proposition $P$) on the natural number $n$: The type
signature basically states "Given a proposition $P$ on $\mathbb{N}$,
a proof of $P_0$, a proof that $P_{(k + 1)}$ follows from $P_k$ and
a natural number $n$, I'll give you a proof of $P_n$."

This understanding of computations as proofs and types as propositions,
by the way, is called the [Curry-Howard
Isomorphism](https://en.wikipedia.org/wiki/Curry-Howard_correspondence).
The regular, simply-typed lambda calculus corresponds to natural
deduction, while $\lambda_{\prod{}}$ corresponds to predicate logic.

### The type system

Should this be called the term system?

Our type inference algorithm, contrary to what you might expect for such
a complicated system, is actually quite simple. Unfortunately, the code
isn't, and thus isn't reproduced in its entirety below.

#### Variables

The simplest case in any type system. The typing judgement that gives
rise to this case is pretty much the identity: $\Gamma \vdash \alpha:
\tau \therefore \Gamma \vdash \alpha: \tau$. If, from the current typing
context we know that $\alpha$ has type $\tau$, then we know that
$\alpha$ has type $\tau$.

```haskell
  Variable x -> do
    ty <- lookupType x -- (I)
    case ty of
      Just t -> pure t -- (II)
      Nothing -> throwError (NotFound x) -- (III)
```

1. Look up the type of the variable in the current context.
2. If we found a type for it, then return that (this is the happy path)
3. If we didn't find a type for it, we raise a type error.

#### `Set`{.haskell}s

Since dtt has a cummulative hierarchy of universes, $\mathtt{Set}_k:
\mathtt{Set}_{(k + 1)}$. This helps us avoid the logical inconsistency
introduced by having _type-in-type_[^2], i.e. $\mathtt{Type}:
\mathtt{Type}$. We say that $\mathtt{Set}_0$ is the type of _small
types_: in fact, $\mathtt{Set}_0$ is where most computation actually
happens, seeing as $\mathtt{Set}_k$ for $k \ge 1$ is reserved for
$\prod$-abstractions quantifying over such types.

```haskell
  Set k -> pure . Set . (+1) $ k
```

#### Type hints

Type hints are the first appearance of the unification engine, by far
the most complex part of dtt's type checker. But for now, suffices to
know that ``t1 `assertEquality` t2``{.haskell} errors if the types t1
and t2 can't be made to _line up_, i.e., unify.

For type hints, we infer the type of given expression, and compare it
against the user-provided type, raising an error if they don't match.
Because of how the unification engine works, the given type may be more
general (or specific) than the inferred one.

```haskell
  TypeHint v t -> do
    it <- infer v
    t `assertEquality` it
    pure t
```

#### $\prod$-abstractions

This is where it starts to get interesting. First, we mandate that the
parameter type is inhabited (basically, that it _is_, in fact, a type).
The dependent product $\displaystyle\prod_{(x : 0)} \alpha$, while allowed by the
language's grammar, is entirely meaningless: There's no way to construct
an inhabitant of $0$, and thus this function may never be applied.

Then, in the context extended with $(\alpha : \tau)$, we require that
the consequent is also a type itself: The function
$\displaystyle\prod_{(x: \mathbb{N})} 0$, while again a valid parse, is
also meaningless.

The type of the overall abstraction is, then, the maximum value of the
indices of the universes of the parameter and the consequent.

```haskell
  Pi x p c -> do
    k1 <- inferSet tx
    k2 <- local (insertType (x, p)) $
      inferSet c
    pure $ Set (k1 `max` k2)
```

#### $\lambda$-abstractions

Much like in the simply-typed lambda calculus, the type of
a $\lambda$-abstraction is an arrow between the type of its parameter
and the type of its body. Of course, $\lambda_{\prod{}}$ incurs the
additional constraint that the type of the parameter is inhabited.

Alas, we don't have arrows. So, we "lift" the lambda's parameter to the
type level, and bind it in a $\prod$-abstraction.

```haskell
  Lam x t b -> do
    _ <- inferSet t
    Pi x t <$> local (insertType (x, t)) (infer b)
```

Note that, much like in the `Pi`{.haskell} case, we type-check the body
in a context extended with the parameter's type.

#### Application

Application is the most interesting rule, as it has to not only handle
inference, it also has to handle instantiation of $\prod$-abstractions.

Instantation is, much like application, handled by $\beta$-reduction,
with the difference being that instantiation happens during type
checking (applying a $\prod$-abstraction is meaningless) and application
happens during normalisation (instancing a $\lambda$-abstraction is
meaningless).

The type of the function being applied needs to be
a $\prod$-abstraction, while the type of the operand needs to be
inhabited. Note that the second constraint is not written out
explicitly: It's handled by the `Pi`{.haskell} case above, and
furthermore by the unification engine.

```haskell
  App e1 e2 -> do
    t1 <- infer e1
    case t1 of
      Pi vr i o -> do
        t2 <- infer e2
        t `assertEquality` i
        N.normalise =<< subst [(vr, e2)] o -- (I)
      e -> throwError (ExpectedPi e) -- (II)
```

1. Notice that, here, we don't substitute the $\prod$-bound variable by
   the type of $e_2$: That'd make us equivalent to System $F$. The whole
   _deal_ with dependent types is that types depend on values, and that
   entirely stems from this one line. By instancing a type variable with
   a value, we allow _types_ to depend on _values_.

2. Oh, and if we didn't get a $\prod$-abstraction, error.

---

You'll notice that two typing rules are missing here: One for handling
`let`{.haskell}s, which was not included because it is entirely
uninteresting, and one for `case ... of`{.haskell} expressions, which
was redacted because it is entirely a mess.

Hopefully, in the future, the typing of `case` expressions is simpler
- if not, they'll probably be replaced by eliminators.

### Unification and Constraint Solving

The unification engine is the man behind the curtain in type checking:
We often don't pay attention to it, but it's the driving force behind it
all. Fortunately, in our case, unification is entirely trivial: Solving
is the hard bit.

The job of the unification engine is to produce a set of constraints
that have to be satisfied in order for two types to be equal. Then, the
solver is run on these constraints to assert that they are logically
consistent, and potentially produce substitutions that _reify_ those
constraints.  
Our solver isn't that cool, though, so it just verifies consitency.

The kinds of constraints we can generate are as in the data type below.

```haskell
data Constraint
  = Instance Var Term -- (1)
  | Equal Term Term -- (2)
  | EqualTypes Type Type -- (3)
  | IsSet Type -- (4)
  deriving (Eq, Show, Ord)
```

1. The constraint `Instance v t`{.haskell} corresponds to a substitution
   between `v` and the term `t`.
2. A constraint `Equal a b`{.haskell} states that the two terms `a` and
   `b` are equal under normalisation.
3. Ditto, but with their _types_ (We normalise, infer, and check for
   equality)
4. A constraint `IsSet t`{.haskell} asserts that the provided type has
   inhabitants.

#### Unification

Unification of most terms is entirely uninteresting. Simply line up the
structures and produce the appropriate equality (or instance)
constraints.

```haskell
unify (Variable a) b = instanceC a b
unify b (Variable a) = instanceC a b
unify (Set a) (Set b) | a == b = pure []
unify (App x y) (App x' y') =
  (++) <$> unify x x' <*> unify y y'
unify (TypeHint a b) (TypeHint c d) = 
  (++) <$> unify a c <*> unify b d
unify a b = throwError (NotEqual a b)
```

Those are all the boring cases, and I'm not going to comment on them.
Similarly boring are binders, which were abstracted out because hlint
told me to.

```haskell
unify (Lam v1 t1 b1) (Lam v2 t2 b2) = unifyBinder (v1, v2) (t1, t2) (b1, b2)
unify (Pi v1 t1 b1) (Pi v2 t2 b2) = unifyBinder (v1, v2) (t1, t2) (b1, b2)
unify (Let v1 t1 b1) (Let v2 t2 b2) = unifyBinder (v1, v2) (t1, t2) (b1, b2)
unifyBinder (v1, v2) (t1, t2) (b1, b2) = do
  (a, b) <- (,) <$> unify (Variable v1) (Variable v2) <*> unify t1 t2
  ((a ++ b) ++) <$> unify b1 b2
```

There are two interesting cases: Unification between some term and a pi
abstraction, and unification between two variables.

```haskell
unify ta@(Variable a) tb@(Variable b)
  | a == b = pure []
  | otherwise = do
  (x, y) <- (,) <$> lookupType a <*> lookupType b
  case (x, y) of
    (Just _, Just _) -> do
      ca <- equalTypesC ta tb
      cb <- equalC ta tb
      pure (ca ++ cb)
    (Just x', Nothing) -> instanceC b x'
    (Nothing, Just x') -> instanceC a x'
    (Nothing, Nothing) -> instanceC a (Variable b)
```

If the variables are syntactically the same, then we're done, and no
constraints have to be generated (Technically you could generate an
entirely trivial equality constraint, but this puts unnecessary pressure
on the solver).

If either variable has a known type, then we generate an instance
constraint between the unknown variable and the known one.

If both variables have a value, we equate their types' types and their
types. This is done mostly for error messages' sakes, seeing as if two
values are propositionally equal, so are their types.

Unification between a term and a $\prod$-abstraction is the most
interesting case: We check that the $\prod$ type abstracts over a type
(i.e., it corresponds to a System F $\forall$ instead of a System
F $\to$), and _instance_ the $\prod$ with a fresh type variable.

```haskell
unifyPi v1 t1 b1 a = do
  id <- refresh Irrelevant
  ss <- isSetC t1
  pi' <- subst [(v1, Variable id)] b1
  (++ ss) <$> unify a pi'

unify a (Pi v1 t1 b1) = unifyPi v1 t1 b1 a
unify (Pi v1 t1 b1) a = unifyPi v1 t1 b1 a
```

#### Solving

Solving is a recursive function of the list of constraints (a
catamorphism!) with some additional state: Namely, a strict map of
already-performed substitutions. Let's work through the cases in reverse
order of complexity (and, interestingly, reverse order of how they're in
the source code).

##### No constraints

Solving an empty list of constraints is entirely trivial.

```haskell
solveInner _ [] = pure ()
```

#### `IsSet`{.haskell}

We infer the index of the universe of the given type, much like in the
inferrence case for $\prod$-abstractions, and check the remaining
constraints.

```haskell
solveInner map (IsSet t:xs) = do
  _ <- inferSet t
  solveInner map xs
```

#### `EqualTypes`{.haskell}

We infer the types of both provided values, and generate an equality
constraint.

```haskell
solveInner map (EqualTypes a b:xs) = do
  ta <- infer a
  tb <- infer b
  solveInner map (Equal ta tb:xs)
```

#### `Equal`{.haskell}

We merely have to check for syntactic equality of the (normal forms of)
terms, because the hard lifting of destructuring and lining up was done
by the unification engine.

```haskell
solveInner map (Equal a b:xs) = do
  a' <- N.normalise a
  b' <- N.normalise b
  eq <- equal a' b'
  if eq
     then solveInner map xs
     else throwError (NotEqual a b)
```

#### `Instance`{.haskell}

If the variable we're instancing is already in the map, and the thing
we're instancing it to _now_ is not the same as before, we have an
inconsistent set of substitutions and must error.

```haskell
solveInner map (Instance a b:xs)
  | a `M.member` map
  , b /= map M.! a
  , Irrelevant /= a
  = throwError $ InconsistentSubsts (a, b) (map M.! a)
```

Otherwise, if we have a coherent set of instances, we add the instance
both to scope and to our local state map and continue checking.

```haskell
  | otherwise =
    local (insertType (a, b)) $
      solveInner (M.insert a b map) xs
```

---

Now that we have both `unify` and `solve`, we can write
`assertEquality`: We unify the two types, and then try to solve the set
of constraints.

```haskell
assertEquality t1 t2 = do
  cs <- unify t1 t2
  solve cs
```

The real implementation will catch and re-throw any errors raised by
`solve` to add appropriate context, and that's not the only case where
"real implementation" and "blag implementation" differ.

### Conclusion

Wow, that was a lot of writing. This conclusion begins on exactly the
500th line of the Markdown source of this article, and this is the
longest article on this blag (by far). However, that's not to say it's
bad: It was amazing to write, and writing `dtt` was also amazing. I am
not good at conclusions.

`dtt` is available under the BSD 3-clause licence, though I must warn
you that the source code hasn't many comments.

I hope you learned nearly as much as I did writing this by reading it.

[^1]: As [proven](https://link.springer.com/chapter/10.1007/BFb0037103) by Gilles Dowek.
[^2]: See [System U](https://en.wikipedia.org/wiki/System_U), also
Girard's paradox - the type theory equivalent of [Russell's
paradox](https://en.wikipedia.org/wiki/Russell%27s_paradox).
