I have a Vector data type which is length indexed
data Vector :: Nat -> Type -> Type where
VNil :: Vector 0 a
VCons :: a -> Vector n a -> Vector (n + 1) a
Now what I would like to do is that every single element in the list contains a function one type a to a type b but that type b is different for every index in the vector.
hMap :: forall n a. Vec n (forall b. a -> b) -> a -> Vec n (forall b. b)
hMap VNil _ = VNil
hMap (f `VCons` fs) a = f a `VCons` hMap fs a
Now of course this doesn't compile. Because GHC doesn't support impredicative types. Is there some kind of workaround for this? Maybe some kind of HVector? I'm not really familiar with such advanced features but really would like to know how hMap could be implemented.
Any help is appreciated!
The vector type that you've defined, as you've already pointed out, is homogeneous. And you can't really use
forallto turn a homogeneous type into a heterogeneous one, as that's not really what it's for.Instead, what we really need is a heterogeneous vector type. First, let's get the formalities out of the way. We're going to need a lot of GHC extensions by the end of this, so here we go.
I'm assuming the
Nattype you're using comes fromGHC.TypeLitsand theTypetype comes fromData.Kind.Now your original vector type looked like this.
I'm going to propose this instead.
Rather than having a single type of element, we have a list of element types. Note that
xsis a list (as in, the built-in[]type) of types (TypefromData.Kind). Every type defined in Haskell can be lifted to the type level. You've already seen how to lift natural number literals (which are a bit of a special case, due to the special syntax surrounding them), but we can do the same to lists.VNilis only valid for for length zero and the empty type list, whileVConsis valid for a nonempty list of lengthn + 1and is made up of an elementt(the head of the type-level listt ': ts) and a tailVector n ts.There are apostrophes in front of the constructors that we're lifting from the value level to the type level.
'[]is just the value constructor[](i.e. the empty list) lifted to the type level. Without the quote, in a type context,[]is a type constructor of kind* -> *, so we have to lift it to fix the ambiguity. And it's generally good practice to do this, even when it's not ambiguous.I also define
VConsto be right-associative when used in infix form, which will make actually constructing these vectors marginally easier down the road.Now, at this juncture, I would recommend we actually omit the
nparameter entirely. You can do pretty much everything else I suggest here with annparameter, but since we also have the list, we still know how long our vector is statically. So what I would recommend isWe can recover the length at the type-level using a type-level function. In fact, this will be good practice for
hMapin a minute, so let's write it out.When we want to write a function that recurses on a complicated GADT type like this, we usually do so as a typeclass. In this case, we want a type-level function called
VectorLength, so we'll write a typeclass which defines a type family to give us our result.In normal value-level Haskell, you write a type signature, then you write the cases to pattern match against. We do the exact same thing in type-level Haskell. However, our "type signature" is a
classand our cases areinstances. Here's our type signature.Now our base case is an instance for the empty list type-level value
'[]. Read that sentence again and make sure you understand what I'm overloading here.Finally, our recursive case.
A vector containing
(x ': xs)has a length if the tail (which containsxs) has a length. And specifically, the length is1 + VectorLength xs. You can think of our instance's context (HasLength xsin this case) as being a sort of dependency list for our function. Our function needs to callVectorLength xs, so we have to declare that in ourinstanceheader.Now let's write
hMap. This will also be in a typeclass, but it'll be a regular Haskell function, not a type family, since we want to operate at the value level. It still has to be a typeclass since we're going to dispatch on things at the type level (namely, our list of constituent types).Here's the type signature.
That funny little syntax after the vertical bar
|is called a functional dependency, and it really helps with type inference. It says "If I know the valuefs(of functions), then I can deduce the valuebs(of results of those functions)".Now we write our base case.
The empty list of functions can be
hMapped to the empty list of results, and the typeain the middle doesn't matter. Specifically, this mapping takesVNiltoVNil.Moving on to our recursive case.
If we can take
fstobswith an input of typea, then we can also take((a -> b) ': fs)to(b ': bs)with an input of typea.The neat thing here is that, after all of the insane type-level juggling is done, the function bodies are actually remarkably simple and beautiful.
Let's test it out. Here are some functions.
This is why I declared the
infixrdeclaration earlier: So we could chainVConscalls here.Now here's the list of results when we apply those functions to
0.This typechecks, but does it work? Well, to figure that out, we'll need to write some quick
Showinstances to be able to print them out.And finally
And the verdict?
Success!
You can see a complete working example of this code on Try it online!