Consider this addition to @chi to give an additional explanation of the singleton approach ...
I would advise you to read the Hashotism document if you have not already done so. In particular, in section 3.1 of this article, they consider this problem and use it as a motivational example for cases when implicit singleton parameters ( SingI response and NATTY class in the Hashokhism document) are necessary, and not just convenient.
As this applies to your code, the main problem is that pure requires a representation of the runtime of the vector that it should generate, and a variable of type n does not fit the count. The solution is to introduce a new GADT, "singleton", which provides run-time values ββthat correspond directly to the advanced types Next and Zero :
data Natty (n :: Nat) where ZeroTy :: Natty Zero NextTy :: Natty n -> Natty (Next n)
I tried to use roughly the same naming convention as paper: NATTY the same, and ZeroTy and NextTy correspond to Zy and Sy paper.
This explicit singleton is useful in itself. For example, see the definition of vchop in a document. In addition, we can easily write a pure variant that takes an explicit singleton to do its job:
vcopies :: Natty n -> a -> Vector na vcopies ZeroTy _ = Empty vcopies (NextTy n) x = Construct x (vcopies nx)
We cannot use this to define pure yet, though, since the signature pure is defined by a class of type Applicative , and we cannot compress the explicit singlet Natty n .
The solution is to introduce implicit singletones that allow us to get an explicit singleton when necessary, using the NATTY function in the context of the following class type:
class NATTY n where natty :: Natty n instance NATTY Zero where natty = ZeroTy instance NATTY n => NATTY (Next n) where natty = NextTy natty
Now, if we are in the context of Natty n , we can call vcopies natty to provide vcopies its explicit NATTY parameter, which allows us to write:
instance NATTY n => Applicative (Vector n) where (<*>) = vapp pure = vcopies natty
using the definitions of vcopies and NATTY above, and the definition of vapp below:
vapp :: Vector n (a -> b) -> Vector na -> Vector nb vapp Empty Empty = Empty vapp (Construct fc) (Construct xd) = Construct (fx) (vapp cd)
Pay attention to one oddity. We needed to introduce this helper function vapp for an unclear reason. The following non- NATTY matches your case definition and type:
instance Applicative (Vector n) where Empty <*> Empty = Empty Construct fc <*> Construct xd = Construct (fx) (c <*> d) pure = error "Argh! No NATTY!"
If we add a NATTY constraint to define pure :
instance NATTY n => Applicative (Vector n) where Empty <*> Empty = Empty Construct fc <*> Construct xd = Construct (fx) (c <*> d) pure = vcopies natty
the definition (<*>) no longer checks the type. The problem is that the Natty n restriction on the left side of the second case (<*>) does not imply an automatic restriction on NATTY n1 on the right side (where Next n ~ n1 ), so the GHC does not want to let us call (<*>) on the right parts. In this case, since the restriction is not actually required after using it for the first time, the helper function without the NATTY restriction, namely vapp , works around the problem.
@chi uses case matching on NATTY and the NATTY helper function as an alternative solution. Here, the equivalent code will use a helper function that turns an explicit singleton into an implicit NATTY context:
withNATTY :: Natty n -> (NATTY n => a) -> a withNATTY ZeroTy a = a withNATTY (NextTy n) a = withNATTY na
allows us to write:
instance NATTY n => Applicative (Vector n) where Empty <*> Empty = Empty Construct fc <*> Construct xd = case (natty :: Natty n) of NextTy n -> withNATTY n $ Construct (fx) (c <*> d) pure x = case (natty :: Natty n) of ZeroTy -> Empty NextTy n -> Construct x (withNATTY n $ pure x)
This will require ScopedTypeVariables and RankNTypes .
In any case, adhering to the auxiliary functions, the complete program is as follows:
{-
Compliance with the singletons library is as follows:
$(singletons [d| data Nat = Next Nat | Zero |])
automatically generates singletones (with SZero and SNat instead of ZeroTy and NATTY , and with SNat type instead of NATTY ) and an implicit single-point class (called SingI instead of from NATTY and using the sing function instead of NATTY ), giving the full program:
{-
For more on what the singletons library does and how it is built, I would suggest reading the Introduction to Singletons .