All of this working with Vec
is fun and all, but it surely seems impractical. After all, if the length of a list is known at compile time, why not just use an array? (Yes, Haskell has two different implementations of arrays, both with a packed memory representation.) Indeed, this is a good question. The convenience of linked lists is that we don’t need to know their lengths up front.
It turns out that we don’t need to know a Vec
’s length at compile time. The secret is to use existentials.
Let’s get our Literate Haskell header out of the way:
> {-# LANGUAGE GADTs, TypeInType, StandaloneDeriving, TypeOperators #-}
> {-# OPTIONS_GHC -Wincomplete-patterns #-}
> module Existentials where
> import Data.Kind ( Type )
> import Prelude hiding ( reverse, filter )
> import Text.Read ( readMaybe )
> data Nat where
> Zero :: Nat
> Succ :: Nat -> Nat
> data Vec :: Nat -> Type -> Type where
> Nil :: Vec Zero a
> (:>) :: a -> Vec n a -> Vec (Succ n) a
> infixr 5 :>
> deriving instance Show a => Show (Vec n a)
Vec
’s lengthSuppose we want to write a program that asks the user for a bunch of numbers, continuing to read in numbers until a non-number is entered. We will put these numbers in a list, reverse the list, and print out the reversed list. This is perhaps not the most inspiring of programs, but it serves to show how existentials are useful. (Indeed, without existentials, little of this fancy type stuff would be useful!)
Naturally, instead of using Data.List
’s reverse
function, we want to use our reverse
on Vec
s. This Vec
reverse
is better than Data.List
’s: GHC can verify that our reverse
does not change the length of the list; Data.List
’s function offers no such reassurance. To pull this off, we’d like to write
toVec :: [a] -> Vec n a
toVec [] = Nil
toVec (x : xs) = x :> toVec xs
The problem is that this function cannot be written. When we write a type variable in a type signature, that is understood to be universally quantified. That is, the definition should work for all choices of that variable. (We can see this when we think about a function bad :: a -> b
. There is no implementation that works for all a
and b
, even though bad x = not x
works for some a
and some b
.) Indeed, compiling the definition above gives us
• Couldn't match type ‘n’ with ‘'Succ n0’
‘n’ is a rigid type variable bound by
the type signature for:
toVec :: forall a (n :: Nat). [a] -> Vec n a
This comes from the second equation, where GHC wants the result of :>
to be a Vec (Succ n0) a
for some n0
, but all we know is that the result should have the index n
. The first equation leads to a similar error, but about Zero
.
Instead, we need an existential type, like this:
> data ExVec :: Type -> Type where
> ExVec :: Vec n a -> ExVec a
The ExVec
constructor takes any Vec n a
and packs it into a ExVec a
. Note that the n
does not appear in the result. That’s what makes the n
existential. When we have a value of type ExVec a
, then we know that there exists an n
such that the ExVec
contains a Vec n a
, but we don’t know what that n
is – it’s not in the type.
Let’s walk through a few examples to understand this better. (This is called toVec1
because we’re going to refine this later into toVec
.)
> toVec1 :: [a] -> ExVec a
> toVec1 [] = ExVec Nil
> toVec1 (x:xs) = case toVec1 xs of ExVec xs' -> ExVec (x :> xs')
This function successfully converts a list into a Vec
. Note that the output length is not mentioned in the return type of toVec1
. In this way, an existential type hides one or more type variables. In the body of the function, we must be careful to pack and unpack the Vec
into its ExVec
holder.
In contrast to toVec1
’s successful use of an existential, here’s a poor usage:
bad (ExVec vec) = vec
I’ve cleverly avoided giving a type to bad
, but perhaps GHC can infer it. No dice:
• Couldn't match expected type ‘t1’ with actual type ‘Vec n t’
because type variable ‘n’ would escape its scope
This (rigid, skolem) type variable is bound by
a pattern with constructor:
ExVec :: forall a (n :: Nat). Vec n a -> ExVec a,
Well, what would the type of bad
be? It would have to be something like ExVec a -> Vec n a
, where the n
is the n
found by looking at the Vec
stored in the ExVec
. Of course, this type is inexpressible, leading to the error message above. (It complains about scoping. When we reveal the Vec
inside an ExVec
package, that Vec
’s index n
is in scope – but only for the length of the pattern match. The type of the function we’re in is clearly not part of that scope.)
The bottom line here: we can pattern-match against existential constructors, but we must be careful that any type variables that come into scope don’t escape. In toVec1
, this happens by re-packing the result into another ExVec
.
Vec
Now that we have toVec1
, we can indeed write a real program over Vec
s.
First, we’ll need a function that reads in numbers from the user until a non-number is entered:
> readNumbers :: IO [Integer]
> readNumbers = do
> line <- getLine
> case readMaybe line of
> Nothing -> return []
> Just n -> do
> more_nums <- readNumbers
> return (n : more_nums)
We’ll need a few more functions on Vec
:
> snoc :: Vec n a -> a -> Vec (Succ n) a
> snoc Nil x = x :> Nil
> snoc (y :> ys) x = y :> (snoc ys x)
> -- Yes, this is slow. But it's just so much easier to write!
> reverse :: Vec n a -> Vec n a
> reverse Nil = Nil
> reverse (x :> xs) = snoc (reverse xs) x
> fromVec :: Vec n a -> [a]
> fromVec Nil = []
> fromVec (x :> xs) = x : fromVec xs
Now, we can write a main
that does all the rest of the work.
> main :: IO ()
> main = do
> putStrLn "Enter some numbers, one per line. To stop, enter anything else."
> nums <- readNumbers
> putStrLn ("You gave me " ++ show nums ++ ".")
> let reversed = case toVec1 nums of
> ExVec vnums -> fromVec (reverse vnums)
> putStrLn ("Reversing gives me " ++ show reversed ++ ".")
You can actually compile this module with ghc Existentials.md.lhs -main-is Existentials
to produce an executable that you can run. (The -main-is
tells GHC to look for the module named Existentials
instead of its usual Main
.)
Let’s quickly look at the use of existentials there. The result of toVec1
is scrutinized to reveal a Vec
vnums
of unknown length n
. Because of the scoping rules around existentials, we can’t have n
escape in the type of the right-hand side of the pattern-match. Happily, that right-hand side has type [Integer]
, with no mention of the length. Without the fromVec
, though, we’d be in big trouble. Try removing the fromVec
to see the “would escape its scope” error appear.
To recap what we’ve done here: these notes have all been about adding more types to functions. But if those functions only worked on data structures whose type indices are all known at compile time, they would be barely useful. Here, we’ve taken a list entered in by the user (surely of an unknowable length) and converted it to a length-indexed Vec
by use of an existential data type. That allowed us to use all our length-verified algorithms on the user-provided list, converting back to a list only at the end.
Existentials are useful for more than just main
: they can be used to describe the output of functions whose output length is unknowable. Consider filter
:
filter :: (a -> Bool) -> Vec n a -> Vec ??????????? a
Without knowing the contents of the Vec
and the behavior of the filtering function, it’s impossible to know what the output length should be. So we don’t try. We use an existential:
filter :: (a -> Bool) -> Vec n a -> ExVec a
However, this is a bit dissatisfying, because we know something about the output length: it’s surely no greater than the input length. This information might be useful if, say, we get an index into the output Vec
(of the Fin
type) and then want to use this index on the original input Vec
. It’s sure to be valid. So, we modify the existential type to allow for some extra information about the hidden type index n
:
> data EVec :: (Nat -> Type) -> Type -> Type where
> EVec :: p n -> Vec n a -> EVec p a
The idea here is that EVec
is parameterized by a predicate over Nat
s (called p
), as well as the element type a
. Evidence for this predicate is then stored in the EVec
.
It’s surely time for an example. First, we’ll define a type of evidence that one number is greater than or equal to another:
> data (:>=:) :: Nat -> Nat -> Type where
> GTEZero :: n :>=: Zero
> GTESucc :: n :>=: m -> Succ n :>=: Succ m
This type is somewhat like the :~:
type that proves that one type is equal to another. It says that there are two ways for n
to be greater-than-or-equal-to (gte) m
: either m
is Zero
, or both n
and m
are the successors of other numbers, n'
and m'
, and n'
gte m'
.
Before we can use this type to write filter
, we’ll need to prove a key fact about :>=:
:
> gteSuccLeft :: (n :>=: m) -> (Succ n :>=: m)
> gteSuccLeft GTEZero = GTEZero
> gteSuccLeft (GTESucc gte) = GTESucc (gteSuccLeft gte)
This says that, whenever we know n :>=: m
, we can prove Succ n :>=: m
. This fact should not be surprising, but it can be established only by induction from the definition of :>=:
; there is no way for GHC to “guess” this fact.
We can now use these definitions to write filter
:
> filter :: (a -> Bool) -> Vec n a -> EVec ((:>=:) n) a
> filter _ Nil = EVec GTEZero Nil
> filter f (x :> xs) = case filter f xs of
> EVec gte xs'
> | f x -> EVec (GTESucc gte) (x :> xs')
> | otherwise -> EVec (gteSuccLeft gte) xs'
First, examine the type. The first parameter to EVec
is ((:>=:) n)
. This takes the prefix form of the type operator :>=:
and applies it to n
. Because it’s in prefix notation, the n
is the first argument to (:>=:)
. At the term level, we might write the section (n :>=:)
, but sections don’t work in types.
Now, let’s look at the term-level definition. In the Nil
case, we must provide evidence that Zero :>=: Zero
. That’s easy, as GTEZero
provides that evidence.
In the :>
case, we recur on xs
and then we have to decide whether or not to include x
in the output. If f x
holds, include x
and use the proof on shorter lists, gte
to prove the greater-than relationship on larger lists, using GTESucc
. If f x
does not hold, then we need to use n :>=: m
to prove Succ n :>=: m
. This is because the input list is getting longer, but the output list is staying the same. Our inductive proof / recursive function gteSuccLeft
does the job for us.
By parameterizing EVec
with a property that should hold of the hidden n
parameter, we can give a more precise type to filter
. But we still sometimes want to write a function like toVec
, where any output length is acceptable. All we need to do is define a universal property:
> data AlwaysTrue :: Nat -> Type where
> Always :: AlwaysTrue n
This says that Always
is a proof of AlwaysTrue n
, for any n
. Our original ExVec a
could now be written EVec AlwaysTrue a
. And indeed we’ll do that for toVec
:
> toVec :: [a] -> EVec AlwaysTrue a
> toVec [] = EVec Always Nil
> toVec (x:xs) = case toVec xs of EVec _ xs' -> EVec Always (x :> xs')
It is perhaps annoying to use this Always
constructor in this case, but this problem can be mitigated with type synonyms and pattern synonyms, but the details would bring us too far afield for now.