List as a constraint variable in a Haskell type signature?

Viewed 124

Rather than declaring this type-signature:

g :: Enum a => [a] -> [a]

I'd like to use this style:

h :: (Enum a, Functor f) => f a -> f a

except to swap Functor out with []. (e.g. because I might be happy to be less general in my implementation for the time-being).

Although I'd like to, I can't use the following,

i :: (Enum a, [] f) => f a -> f a 
-- compilation error: Expected kind ‘* -> *’, but ‘f’ has kind ‘*’

since the [] f is probably interpreted as a list of f, rather than f being interpreted as a constraint-variable for a list of something.

The following works, but I'd like to know if it is possible to declare a List as an actual constraint-variable in a Haskell type-signature.

j :: (Enum a) => [] a -> [] a
2 Answers

[] is not a constraint and it never will be. In Haskell, every type has a kind, which is kind of like a type of type. It's one level up.

Things like [Int], String, and Double -> Double are concrete types. We say they have kind Type. Something like [] or Maybe has kind Type -> Type; it takes a type as argument and then produces a type itself.

On the other hand, something like Functor is fundamentally different. Functor has kind

Functor :: (Type -> Type) -> Constraint

So, first off, it takes a parameterized type, like [] or Maybe as argument. But even once it does so, it produces a Constraint, a fundamentally different thing than a Type. Now, when we write a type signature

j :: (Enum a) => [] a -> [] a

The things on the left of the => are always constraints and the thing on the right of the => is always a Type. It makes no more sense to put [] a on the left-hand side of => than it does to put Functor a on the right-hand side.

It sounds like the thing you're trying to express is "this thing has to be a list", in which case either of the following will suffice

j :: (Enum a) => [a] -> [a]
j :: (Enum a) => [] a -> [] a

the two are equivalent but the former is much more idiomatic. If you come along later and decide "oh wait, I can make this work for any functor, you can change the type signature to

j :: (Enum a, Functor f) => f a -> f a

And this won't break any of the code that's calling j, as any code that was calling it with a list before will still be calling it with a valid Functor instance.


Note: You'll see some old literature that refers to the kind *. Some literature (and even GHC in some places) will say that Maybe has kind * -> *. For these purposes, * is equivalent to Type. Being able to refer to Type as * is an unfortunate historical quirk and complicates the Haskell parser (is Foo * Int equivalent to Foo Type Int or is it multiplication like (*) Foo Int?), so the general recommendation nowadays is to use Type to refer to the kind of types.

You can declare equality constraints as

g :: (Enum a, [] ~ f) => f a -> f a

under suitable language extensions. Read the operator (~) as equality or congruence.

  • Either set one of -XGADTs or -XTypeFamilies at the GHC command line.

  • Or, place either

    {-# LANGUAGE GADTs #-}
    {-# LANGUAGE TypeFamilies #-}
    

    at the top of the file.

Is it really worth it, though?

Related