Covariance and Contravariance, functionally
From Covariance and Contravariance we know that generic classes can be made covariant or contravariant. In fact, the same goes for functors. Consider the following type constructor List
in Haskell.
data List a = Nil | Cons a (List a)
It’s a covariant functor because we could define a function fmap
which takes a function from a
to b
and returns another function from List a
to List b
.
fmap :: (a -> b) -> (List a -> List b)
This is the definition of fmap
instance Functor List where
fmap _ Nil = Nil
fmap f (Cons x t) = Cons (f x) (fmap f t)
A contravariant functor is, by contrast, a type constructor f
which we could define a contramap
taking a function b -> a
and returning another one f a -> f b
.
contramap :: (b -> a) -> (f a -> f b)
An example of contravariant functors is the so called Writer type, which is essentially a function when the return type is fixed, or, Op r
as defined below.
type Op r a = a -> r
The contramap
is defined as
instance Contravariant (Op r) where
-- (b -> a) -> Op r a -> Op r b
-- here f is the b -> a and g is the Op r a (or a -> r)
contramap f g = g . f
Notice that f
applies before g
. Since f
is b -> a
and g
is Op r a
(or a -> r
) and the result type is Op r b
(or b -> r
), we should apply f
first to convert b
to a
and then g
which converts a
to r
. In other words, the contramap
function of the writer type “inserts” the function b -> a
before the function a -> r
.
Covariance and Contravariance, categorically
We know that in category theory, a functor is a mapping between categories that preserves their structures. A functor \(F\)
between two categories is shown below.
However, in programming we generally have only one category. Below is how covariant and contravariant functors work in only one category. A covariant functor maps a -> b
to F a -> F b
, and a contravariant functor maps a -> b
to F b -> F a
.
Note that the word functor is used in both category theory and functional programming. To avoid confusion I refer to functors in functional programming as functional functors. When a single word functor is used, it means functors in category theory.
Below is how covariant and contravariant functors work from the categories perspective. Here the objects are types (like Int
and String
) and the morphisms are functions between types (there are a lot of morphisms between Int
and String
, but only parseInt
is shown as an example). In this example, both List
and Op Bool
(a -> Bool
) are functors.
The List
functor maps any type a
to type List a
and any function f :: a -> b
to fmap f :: List a -> List b
by applying function f
to every element of the list. It preserves the direction of morphisms so it’s covariant. In contrast, the Op Bool
functor maps any type a
to function type a -> Bool
and any function f :: a -> b
to contramap f :: (b -> Bool) -> (a -> Bool)
by “inserting” itself before the b -> Bool
function. It reverses the direction of morphisms so it’s called contravariant.
On the other hand, below is how covariant and contravariant generics work. Here the objects are still types, but the morphisms are subtype relations. In contrast to functional functors, there are only at most one morphism between objects in this category. In this example, both List
and Printer
are functors, but works on a different category than the example above.
The List
functor maps any type a
to type List[a]
and any subtype relation a < b
to List[a] < List[b]
(here <
means “subtype of”). It preserves the direction of morphisms so it’s covariant. In contrast, the Printer
functor maps any type a
to Printer[a]
and any relation a < b
to Printer[b] < Printer[a]
. It reverses the direction of morphisms so it’s called contravariant.
In conclusion, Both functional functors and generic classes are actually functors, and the words covariant and contravariant in both functors and generics essentially describe the covariance/contravariance of the underlying categorical functors. However, they work on different categories. For functors the morphisms are functions but for generics the morphisms are subtype relations.