|
---
|
|
title: Dependent types in Haskell - Sort of
|
|
date: August 23, 2016
|
|
---
|
|
|
|
**Warning**: An intermediate level of type-fu is necessary for understanding
|
|
*this post.
|
|
|
|
The glorious Glasgow Haskell Compilation system, since around version 6.10 has
|
|
had support for indexed type familes, which let us represent functional
|
|
relationships between types. Since around version 7, it has also supported
|
|
datatype-kind promotion, which lifts arbitrary data declarations to types. Since
|
|
version 8, it has supported an extension called `TypeInType`, which unifies the
|
|
kind and type level.
|
|
|
|
With this in mind, we can implement the classical dependently-typed example:
|
|
Length-indexed lists, also called `Vectors`{.haskell}.
|
|
|
|
----
|
|
|
|
> {-# LANGUAGE TypeInType #-}
|
|
|
|
`TypeInType` also implies `DataKinds`, which enables datatype promotion, and
|
|
`PolyKinds`, which enables kind polymorphism.
|
|
|
|
`TypeOperators` is needed for expressing type-level relationships infixly, and
|
|
`TypeFamilies` actually lets us define these type-level functions.
|
|
|
|
> {-# LANGUAGE TypeOperators #-}
|
|
> {-# LANGUAGE TypeFamilies #-}
|
|
|
|
Since these are not simple-kinded types, we'll need a way to set their kind
|
|
signatures[^kind] explicitly. We'll also need Generalized Algebraic Data Types
|
|
(or GADTs, for short) for defining these types.
|
|
|
|
> {-# LANGUAGE KindSignatures #-}
|
|
> {-# LANGUAGE GADTs #-}
|
|
|
|
Since GADTs which couldn't normally be defined with regular ADT syntax can't
|
|
have deriving clauses, we also need `StandaloneDeriving`.
|
|
|
|
> {-# LANGUAGE StandaloneDeriving #-}
|
|
|
|
> module Vector where
|
|
> import Data.Kind
|
|
|
|
----
|
|
|
|
Natural numbers
|
|
===============
|
|
|
|
We could use the natural numbers (and singletons) implemented in `GHC.TypeLits`,
|
|
but since those are not defined inductively, they're painful to use for our
|
|
purposes.
|
|
|
|
Recall the definition of natural numbers proposed by Giuseppe Peano in his
|
|
axioms: **Z**ero is a natural number, and the **s**uccessor of a natural number
|
|
is also a natural number.
|
|
|
|
If you noticed the bold characters at the start of the words _zero_ and
|
|
_successor_, you might have already assumed the definition of naturals to be
|
|
given by the following GADT:
|
|
|
|
< data Nat where
|
|
< Z :: Nat
|
|
< S :: Nat -> Nat
|
|
|
|
This is fine if all you need are natural numbers at the _value_ level, but since
|
|
we'll be parametrising the Vector type with these, they have to exist at the
|
|
type level. The beauty of datatype promotion is that any promoted type will
|
|
exist at both levels: A kind with constructors as its inhabitant types, and a
|
|
type with constructors as its... constructors.
|
|
|
|
Since we have TypeInType, this declaration was automatically lifted, but we'll
|
|
use explicit kind signatures for clarity.
|
|
|
|
> data Nat :: Type where
|
|
> Z :: Nat
|
|
> S :: Nat -> Nat
|
|
|
|
The `Type` kind, imported from `Data.Kind`, is a synonym for the `*` (which will
|
|
eventually replace the latter).
|
|
|
|
Vectors
|
|
=======
|
|
|
|
Vectors, in dependently-typed languages, are lists that apart from their content
|
|
encode their size along with their type.
|
|
|
|
If we assume that lists can not have negative length, and an empty vector has
|
|
length 0, this gives us a nice inductive definition using the natural number
|
|
~~type~~ kind[^kinds]
|
|
|
|
> 1. An empty vector of `a` has size `Z`{.haskell}.
|
|
> 2. Adding an element to the front of a vector of `a` and length `n` makes it
|
|
> have length `S n`{.haskell}.
|
|
|
|
We'll represent this in Haskell as a datatype with a kind signature of `Nat ->
|
|
Type -> Type` - That is, it takes a natural number (remember, these were
|
|
automatically lifted to kinds), a regular type, and produces a regular type.
|
|
Note that, `->` still means a function at the kind level.
|
|
|
|
> data Vector :: Nat -> Type -> Type where
|
|
|
|
Or, without use of `Type`,
|
|
|
|
< data Vector :: Nat -> * -> * where
|
|
|
|
We'll call the empty vector `Nil`{.haskell}. Remember, it has size
|
|
`Z`{.haskell}.
|
|
|
|
> Nil :: Vector Z a
|
|
|
|
Also note that type variables are implicit in the presence of kind signatures:
|
|
They are assigned names in order of appearance.
|
|
|
|
Consing onto a vector, represented by the infix constructor `:|`, sets its
|
|
length to the successor of the existing length, and keeps the type of elements
|
|
intact.
|
|
|
|
> (:|) :: a -> Vector x a -> Vector (S x) a
|
|
|
|
Since this constructor is infix, we also need a fixidity declaration. For
|
|
consistency with `(:)`, cons for regular lists, we'll make it right-associative
|
|
with a precedence of `5`.
|
|
|
|
> infixr 5 :|
|
|
|
|
We'll use derived `Show`{.haskell} and `Eq`{.haskell} instances for
|
|
`Vector`{.haskell}, for clarity reasons. While the derived `Eq`{.haskell} is
|
|
fine, one would prefer a nicer `Show`{.haskell} instance for a
|
|
production-quality library.
|
|
|
|
> deriving instance Show a => Show (Vector n a)
|
|
> deriving instance Eq a => Eq (Vector n a)
|
|
|
|
Slicing up Vectors {#slicing}
|
|
==================
|
|
|
|
Now that we have a vector type, we'll start out by implementing the 4 basic
|
|
operations for slicing up lists: `head`, `tail`, `init` and `last`.
|
|
|
|
Since we're working with complicated types here, it's best to always use type
|
|
signatures.
|
|
|
|
Head and Tail {#head-and-tail}
|
|
-------------
|
|
|
|
Head is easy - It takes a vector with length `>1`, and returns its first
|
|
element. This could be represented in two ways.
|
|
|
|
< head :: (S Z >= x) ~ True => Vector x a -> a
|
|
|
|
This type signature means that, if the type-expression `S Z >= x`{.haskell}
|
|
unifies with the type `True` (remember - datakind promotion at work), then head
|
|
takes a `Vector x a` and returns an `a`.
|
|
|
|
There is, however, a much simpler way of doing the above.
|
|
|
|
> head :: Vector (S x) a -> a
|
|
|
|
That is, head takes a vector whose length is the successor of a natural number
|
|
`x` and returns its first element.
|
|
|
|
The implementation is just as concise as the one for lists:
|
|
|
|
> head (x :| _) = x
|
|
|
|
That's it. That'll type-check and compile.
|
|
|
|
Trying, however, to use that function on an empty vector will result in a big
|
|
scary type error:
|
|
|
|
```plain
|
|
Vector> Vector.head Nil
|
|
|
|
<interactive>:1:13: error:
|
|
• Couldn't match type ‘'Z’ with ‘'S x0’
|
|
Expected type: Vector ('S x0) a
|
|
Actual type: Vector 'Z a
|
|
• In the first argument of ‘Vector.head’, namely ‘Nil’
|
|
In the expression: Vector.head Nil
|
|
In an equation for ‘it’: it = Vector.head Nil
|
|
```
|
|
|
|
Simplified, it means that while it was expecting the successor of a natural
|
|
number, it got zero instead. This function is total, unlike the one in
|
|
`Data.List`{.haskell}, which fails on the empty list.
|
|
|
|
< head [] = error "Prelude.head: empty list"
|
|
< head (x:_) = x
|
|
|
|
Tail is just as easy, except in this case, instead of discarding the predecessor
|
|
of the vector's length, we'll use it as the length of the resulting vector.
|
|
|
|
This makes sense, as, logically, getting the tail of a vector removes its first
|
|
length, thus "unwrapping" a level of `S`.
|
|
|
|
> tail :: Vector (S x) a -> Vector x a
|
|
> tail (_ :| xs) = xs
|
|
|
|
Notice how neither of these have a base case for empty vectors. In fact, adding
|
|
one will not typecheck (with the same type of error - Can't unify `Z`{.haskell}
|
|
with `S x`{.haskell}, no matter how hard you try.)
|
|
|
|
Init {#init}
|
|
----
|
|
|
|
What does it mean to take the initial of an empty vector? That's obviously
|
|
undefined, much like taking the tail of an empty vector. That is, `init` and
|
|
`tail` have the same type signature.
|
|
|
|
> init :: Vector (S x) a -> Vector x a
|
|
|
|
The `init` of a singleton list is nil. This type-checks, as the list would have
|
|
had length `S Z` (that is - 1), and now has length `Z`.
|
|
|
|
> init (x :| Nil) = Nil
|
|
|
|
To take the init of a vector with more than one element, all we do is recur on
|
|
the tail of the list.
|
|
|
|
> init (x :| y :| ys) = x :| Vector.init (y :| ys)
|
|
|
|
That pattern is a bit weird - it's logically equivalent to `(x :|
|
|
xs)`{.haskell}. But, for some reason, that doesn't make the typechecker happy,
|
|
so we use the long form.
|
|
|
|
Last {#last}
|
|
----
|
|
|
|
Last can, much like the list version, be implemented in terms of a left fold.
|
|
The type signature is like the one for head, and the fold is the same as that
|
|
for lists. The foldable instance for vectors is given [here](#Foldable).
|
|
|
|
> last :: Vector (S x) a -> a
|
|
> last = foldl (\_ x -> x) impossible where
|
|
|
|
Wait - what's `impossible`? Since this is a fold, we do still need an initial
|
|
element - We could use a pointful fold with the head as the starting point, but
|
|
I feel like this helps us to understand the power of dependently-typed vectors:
|
|
That error will _never_ happen. Ever. That's why it's `impossible`!
|
|
|
|
> impossible = error "Type checker, you have failed me!"
|
|
|
|
That's it for the basic vector operations. We can now slice a vector anywhere
|
|
that makes sense - Though, there's one thing missing: `uncons`.
|
|
|
|
Uncons {#uncons}
|
|
------
|
|
|
|
Uncons splits a list (here, a vector) into a pair of first element and rest.
|
|
With lists, this is generally implemented as returning a `Maybe`{.haskell} type,
|
|
but since we can encode the type of a vector in it's type, there's no need for
|
|
that here.
|
|
|
|
> uncons :: Vector (S x) a -> (a, Vector x a)
|
|
> uncons (x :| xs) = (x, xs)
|
|
|
|
Mapping over Vectors {#functor}
|
|
====================
|
|
|
|
We'd like a `map` function that, much like the list equivalent, applies a
|
|
function to all elements of a vector, and returns a vector with the same length.
|
|
This operation should hopefully be homomorphic: That is, it keeps the structure
|
|
of the list intact.
|
|
|
|
The `base` package has a typeclass for this kind of morphism, can you guess what
|
|
it is? If you guessed Functor, then you're right! If you didn't, you might
|
|
aswell close the article now - Heavy type-fu inbound, though not right now.
|
|
|
|
The functor instance is as simple as can be:
|
|
|
|
> instance Functor (Vector x) where
|
|
|
|
The fact that functor expects something of kind `* -> *`, we need to give the
|
|
length in the instance head - And since we do that, the type checker guarantees
|
|
that this is, in fact, a homomorphic relationship.
|
|
|
|
Mapping over `Nil` just returns `Nil`.
|
|
|
|
> f `fmap` Nil = Nil
|
|
|
|
Mapping over a list is equivalent to applying the function to the first element,
|
|
then recurring over the tail of the vector.
|
|
|
|
> f `fmap` (x :| xs) = f x :| (fmap f xs)
|
|
|
|
We didn't really need an instance of Functor, but I think standalone map is
|
|
silly.
|
|
|
|
Folding Vectors {#foldable}
|
|
===============
|
|
|
|
The Foldable class head has the same kind signature as the Functor class head:
|
|
`(* -> *) -> Constraint` (where `Constraint` is the kind of type classes), that
|
|
is, it's defined by the class head
|
|
|
|
< class Foldable (t :: Type -> Type) where
|
|
|
|
So, again, the length is given in the instance head.
|
|
|
|
> instance Foldable (Vector x) where
|
|
> foldr f z Nil = z
|
|
> foldr f z (x :| xs) = f x $ foldr f z xs
|
|
|
|
This is _exactly_ the Foldable instance for `[a]`, except the constructors are
|
|
different. Hopefully, by now you've noticed that Vectors have the same
|
|
expressive power as lists, but with more safety enforced by the type checker.
|
|
|
|
Conclusion
|
|
==========
|
|
|
|
Two thousand words in, we have an implementation of functorial, foldable vectors
|
|
with implementations of `head`, `tail`, `init`, `last` and `uncons`. Since
|
|
going further (implementing `++`, since a Monoid instance is impossible) would
|
|
require implementing closed type familes, we'll leave that for next time.
|
|
|
|
Next time, we'll tackle the implementation of `drop`, `take`, `index` (`!!`, but
|
|
for vectors), `append`, `length`, and many other useful list functions.
|
|
Eventually, you'd want an implementation of all functions in `Data.List`. We
|
|
shall tackle `filter` in a later issue.
|
|
|
|
[^kind]: You can read about [Kind polymorphism and
|
|
Type-in-Type](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#kind-polymorphism-and-type-in-type)
|
|
in the GHC manual.
|
|
|
|
[^kinds]: The TypeInType extension unifies the type and kind level, but this
|
|
article still uses the word `kind` throughout. This is because it's easier to
|
|
reason about types, datatype promotion and type familes if you have separate
|
|
type and kind levels.
|