I have been playing with vectors and matrices where the size is encoded in their type, using the new DataKinds
extension. It basically goes like this:
data Nat = Zero | Succ Nat
data Vector :: Nat -> * -> * where
VNil :: Vector Zero a
VCons :: a -> Vector n a -> Vector (Succ n) a
Now we want typical instances like Functor
and Applicative
. Functor
is easy:
instance Functor (Vector n) where
fmap f VNil = VNil
fmap f (VCons a v) = VCons (f a) (fmap f v)
But with the Applicative
instance there is a problem: We don't know what type to return in pure. However, we can define the instance inductively on the size of the vector:
instance Applicative (Vector Zero) where
pure = const VNil
VNil <*> VNil = VNil
instance Applicative (Vector n) => Applicative (Vector (Succ n)) where
pure a = VCons a (pure a)
VCons f fv <*> VCons a v = VCons (f a) (fv <*> v)
However, even though this instance applies for all vectors, the type checker doesn't know this, so we have to carry the Applicative
constraint every time we use the instance.
Now, if this applied only to the Applicative
instance it wouldn't be a problem, but it turns out that the trick of recursive instance declarations is essential when programming with types like these. For instance, if we define a matrix as a vector of row vectors using the TypeCompose library,
type Matrix nx ny a = (Vector nx :. Vector ny) a
we have to define a type class and add recursive instance declarations to implement both the transpose and matrix multiplication. This leads to a huge proliferation of constraints we have to carry around every time we use the code, even though the instances actually apply to all vectors and matrices (making the constraints kind of useless).
Is there a way to avoid having to carry around all these constraints? Would it be possible to extend the type checker so that it can detect such inductive constructions?