Why does parametrized type instance works without specifying type parameter
instance Functor A where
instead ofinstance Functor (A a) where
. Why does it work ?
I find this easier to understand using GHC's kinding system. Let's start from a simple case, and experiment in GHCi:
> :k Eq Int
Eq Int :: Constraint
This tells us that Eq Int
is a constraint, some property that might be verified during type checking. Indeed, if we type check (12 :: Int) == (42 :: Int)
, the compiler will verify that integers can be compared, resolving the constraint Eq Int
.
What is Eq
alone, the name of the class without the Int
parameter?
> :k Eq
Eq :: * -> Constraint
This tells us that Eq
can be thought of a function from types (*
is the kind of types) to constraint.
Indeed, in Eq Int
, Int
is a type, so we have Int :: *
making Int
a well-kinded argument to pass to Eq
.
Enough of type classes, what about type constructors?
> :k Maybe Int
Maybe Int :: *
No surprise, Maybe Int
is a type
> :k Maybe
Maybe :: * -> *
Maybe
instead, is a function from types to types (*->*
). This is indeed what the Maybe
type constructor does: mapping a type (Int
) to a type (Maybe Int
).
Back to the original question. Why can't we write instance Functor (A a)
but we can instead write instance Functor A
? Well, we have that
> :k A Int
A Int :: *
> :k A
A :: * -> *
and, most importantly,
> :k Functor
Functor :: (* -> *) -> Constraint
This tells us the the kind of the Functor
type class is not the same kind of the Eq
type class. Eq
expects a type as an argument, while Functor
expects something of kind (* -> *)
as an argument. A
fits that kind, while A Int
does not.
This happens when, in the definition of the class, the argument is applied to some other type. E.g.
class C1 a where
foo :: a -> Bool
results in C1 :: * -> Constraint
. Instead,
class C2 f where
bar :: f Int -> Bool
results in C2 :: (* -> *) -> Constraint
, since f
is not itself used as a type, f Int
is, so f
must be a parameterized type of kind * -> *
.
When should you neglect the type parameter?
The type parameter is not "neglected". Let us first take a look at the Functor
type class:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Notice the f a
and f b
in the type signature. We thus here "construct types" for the type signature of fmap
.
Here f
is thus basically a function that takes a type and transforms it into a type. For example if f ~ A
if a ~ Int
, then f a
generates the A Int
type.
Learn You a Haskell for the Greater Good! actually explains this in its chapter about Functors, Applicatives and Monoids:
Many times, we want to make our types instances of certain type classes, but the type parameters just don't match up for what we want to do. It's easy to make
Maybe
an instance ofFunctor
, because theFunctor
type class is defined like this:class Functor f where fmap :: (a -> b) -> f a -> f b
So we just start out with:
instance Functor Maybe where
And then implement
fmap
. All the type parameters add up because theMaybe
takes the place off
in the definition of theFunctor
type class and so if we look atfmap
like it only worked onMaybe
, it ends up behaving like:fmap :: (a -> b) -> Maybe a -> Maybe b
(...)