Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Declaring instances of parameterized type synonyms

Tags:

types

haskell

I have a bunch of functions that work on Vectors, i.e. lists with type-enforced lengths.

I'm trying to make my types easier to write, i.e. instead of writing

foo :: (Fold Integer v, Map Integer Integer v v, ...) => ...

I'm declaring a new class NList so I can just write foo :: NList v Integer => ...

The (simplified) class looks like this:

class ( Fold (v i) i
      , Map i i (v i) (v i)
      , Map i (Maybe i) (v i) (v (Maybe i))
      ) => NList v i

As you can see, I have to keep the "vector" type separate from the "item" type (i.e. v separate from i) so that I can do things like Map onto a Maybe vector.

As such, v must have kind * -> *, and i kind *.

However, when I try to instantiate it with vectors like so:

instance NList Vec2 Integer
instance NList Vec3 Integer
...

I get the following error:

Type synonym `Vec2' should have 1 argument, but has been given none
In the instance declaration for `NList Vec2 Integer'

Now, I'm very new to type-level programming, and I understand that I'm likely doing this in a very backward fashion. Is it possible to instantiate a type synonym like this? Do any type-wizards have suggestions for better ways to accomplish my goals?

like image 789
So8res Avatar asked Feb 15 '12 08:02

So8res


1 Answers

The problem here is that Vec2 and Vec3 are presumably declared as something like this:

type Vec2 a = Vec (S (S Z)) a
type Vec3 a = Vec (S (S (S Z))) a

Type synonyms cannot be partially-applied, for various arcane reasons (they would lead to type-level lambdas, which wreck havoc with all sorts of things related to instance resolution and inference — imagine if you could define type Id a = a and make it an instance of Monad).

That is to say, you can't make Vec2 an instance of anything, because you can't use Vec2 as if it was a fully-fledge type constructor with the kind * -> *; it's effectively a type-level "macro" that can only be fully applied.

However, you can define type synonyms as partial applications themselves:

type Vec2 = Vec (S (S Z))
type Vec3 = Vec (S (S (S Z)))

These are equivalent, except that your instances will be permitted.

If your Vec3 type actually looks like

type Vec3 a = Cons a (Cons a (Cons a Nil)))

or similar, then you're out of luck; you'll have to use a newtype wrapper if you want to give any instances. (On the other hand, you should be able to avoid defining instances directly on these types entirely by giving instances for Nil and Cons instead, allowing you to use Vec3 as an instance.)

Note that with GHC 7.4's new constraint kinds, you can avoid a separate type entirely, and simply define a constraint synonym:

type NList v i =
    ( Fold (v i) i
    , Map i i (v i) (v i)
    , Map i (Maybe i) (v i) (v (Maybe i))
    )

As far as your approach in general goes, it should basically work fine; the same general idea is used by the Vec package. The huge number of classes and large contexts can make error messages very confusing and slow down compilation, but such is the nature of type-level hackery. However, if you have a base Vec type defined as the usual GADT:

data Vec n a where
    Nil :: Vec Z a
    Succ :: a -> Vec n a -> Vec (S n) a

then you don't actually need any typeclasses at all. If it's defined in some other manner but is still parametrised on a type-level natural, then you can replace all the classes with one:

data Proxy s = Proxy

class Nat n where
    natElim
        :: ((n ~ Z) => r)
        -> (forall m. (n ~ S m, Nat m) => Proxy m -> r)
        -> Proxy n
        -> r

This should allow you to eliminate the contexts altogether, but makes defining the operations on the vectors a bit trickier.

like image 53
ehird Avatar answered Oct 27 '22 03:10

ehird