This post assumes prior knowledge of - the Functor
class / concept - the functor instance for (->) r
Why
Not all higher kinded types * -> *
can have a Functor
instance. While types like Maybe a
, (x, a)
, r -> a
, Either e a
and [a]
are Functors
in a
, there are some types that cannot have a Functor
instance. A good example is Predicate
:
newtype Predicate a = Predicate { getPredicate :: a -> Bool }
We call this type a predicate in a
because, given some value of type a
we can obtain a True
or a False
. So, for example: - Predicate (> 10)
is a predicate in Int
which returns true if the number is greater than 10, - Predicate (== "hello")
is a predicate in String
which returns true if the string is equal to "hello", and - Predicate not
is a predicate in Bool
which returns the negation of the boolean value it receives.
We can try writing a Functor
instance and see what we can learn:
instance Functor Predicate where
fmap :: (a -> b) -> Predicate a -> Predicate b
fmap f (Predicate g) =
Predicate
$ \b -> _welp
As the type hole above would suggest, we need to return a Bool
value, and we have: - b :: b
- f :: a -> b
- g :: a -> Bool
There is no way we can combine these terms at all, let alone in such a way as to obtain a Bool
value. The only way we would be able to obtain a Bool
value is by calling g
, but for that, we need an a
-- but all we have is a b
.
What if f
was reversed, though? If we had f' :: b -> a
, then we could apply b
to it f' b :: a
and then pass it to g
to get a Bool
. Let's write this function outside of the Functor
class:
mapPredicate :: (b -> a) -> Predicate a -> Predicate b
Predicate g) =
mapPredicate f (Predicate
$ \b -> g (f b)
This looks very weird, compared to Functor
s, right? If you want to go from Predicate a
to Predicate b
, you need a b -> a
function?
Exercise 1: fill in the typed hole _e1
:
greaterThan10 :: Predicate Int
= Predicate (> 10)
greaterThan10
exercise1 :: Predicate String
= mapPredicate _e1 greaterThan10 exercise1
Exercise 2: write mapShowable
for the following type:
newtype Showable a = Showable { getShowable :: a -> String }
mapShowable :: (b -> a) -> Showable a -> Showable b
Exercise 3: Use mapShowable
and showableInt
to implement exercise3
such that getShowable exercise3 True
is "1"
and getShowable exercise3 False
is "2"
.
showableInt :: Showable Int
= Showable show
showableInt
exercise3 :: Showable Bool
= exercise3
How
Predicate
and Showable
are very similar, and types like them admit a Contravariant
instance. Let's start by looking at it:
class Contravariant f where
contramap :: (b -> a) -> f a -> f b
The instances for Predicate
and Showable
are trivial: they are exactly mapPredicate
and mapShowable
. The difference between Functor
and Contravariant
is exactly the function they receive: one is "forward" and the other is "backward", and it's all about how the data type is defined.
All Functor
types have their type parameter a
in what we call a positive position. This usually means there can be some a
available in the type (which is always the case for tuples, or sometimes the case for Maybe
, Either
or []
). It can also mean we can obtain an a
, like is the case for r -> a
. Sure, we need some r
to do that, but we are able to obtain an a
afterwards.
On the opposite side, Contravariant
types have their type parameter a
in what we call a negative position: they need to consume an a
in order to produce something else (a Bool
or a String
for our examples).
Exercise 4: Look at the following types and decide which can have a Functor
instance and which can have a Contravariant
instance. Write the instances down:
data T0 a = T0 a Int
data T1 a = T1 (a -> Int)
data T2 a = T2L a | T2R Int
data T3 a = T3
data T4 a = T4L a | T4R a
data T5 a = T5L (a -> Int) | T5R (a -> Bool)
As with Functor
s, we can partially apply higher kinded types to write a Contravariant
instance. The most common case is for the flipped version of ->
:
newtype Op a b = Op { getOp :: b -> a }
While a -> b
has a Functor
instance, because the type is actually (->) a b
, and b
is in a positive position, its flipped version has a Contravariant
instance.
Exercise 5: Write the Contravariant
instance for Op
:
instance Contravariant (Op r) where
contramap :: (b -> a) -> Op r a -> Op r b
Exercise 6: Write a Contravariant
instance for Comparison
:
newtype Comparison a = Comparison { getComparison :: a -> a -> Ordering }
Exercise 7: Can you think of a type that has both Functor
and Contravariant
instances?
Exercise 8: Can you think of a type that can't have a Functor
nor a Contravariant
instance? These types are called Invariant
functors.