Covariant vs Contravariant in Functional Programming
When you see a type like f a
, what comes to your mind? Most people think of types like Array a or Maybe a
. You might also picture f
as a kind of “box” that holds some values, so f Int
would be a box containing Int.
But what if I told you that’s not the only way to think about it? f a
could also be a box that takes a
rather than containing a
. This distinction is key to understanding covariant and contravariant functors.
Covariant
When you see a type f a
, and you can convert it to a f b
using a transformation function a -> b
then we can say that f is covariant on its type argument
So the typical functor type class in purescript/haskell is a Covariant Functor,
class Functor f where
map :: forall a b. (a -> b) -> f a -> f b
A covariant functor f a
indicates that it holds or emits values of type a
. Given a function that converts a -> b
, it can then hold or emit values of type b
.
Example
Consider f
as a box, and a
as apple and b
as apple juice
map :: (Apple -> AppleJuice) -> Box Apple -> Box AppleJuice
Here Box
is a covariant functor because it applies the function Apple -> AppleJuice
across its contents, thus turning a Box Apple into a Box AppleJuice.
The core idea of covariance is how it preserves the direction of function application. I mean we can write map as
map :: (Apple -> AppleJuice) -> (Box Apple -> Box AppleJuice)
This illustrates how, given a function from Apple -> AppleJuice
, we are able to obtain a function from Box Apple -> Box AppleJuice
only because Box
is a covariant functor.
Contravariant
But have you seen cmap
which is a method of the Contravariant
typeclass?
class Contravariant f where
cmap :: forall a b. (b -> a) -> f a -> f b
At first glance, if you think of f a
as something that holds a value of type a
, then cmap
seems counterintuitive.
It might seem like cmap
suggests taking a box that contains apples and, given a function that turns apple juice into apples, transforms it into a box that contains apple juice. However, if you read it again you will realize that isn't possible.
This confusion stems from misunderstanding the role of f
in Contravariant
. In this context, f
does not hold or emit values of type a
; rather, it accepts them. So, let's rephrase:
When you have a box that accepts apples (f a
), and you provide a function that turns apple juice into apples (b -> a
), cmap
can then give you a box that accepts apple juice (f b
).
In essence, whereas covariance with map
preserves the direction of the function:
map :: (a -> b) -> (f a -> f b)
Contravariance with cmap
reverses the direction of the function:
cmap :: (b -> a) -> (f a -> f b)
Comparing cmap & map:
cmap
in the Contravariant type class works by transforming the input that a structure is designed to accept. For instance, consider aPredicate
that filters even numbers:
newtype Predicate a = Predicate (a -> Boolean)
evenNumberOnly :: Predicate Int
evenNumberOnly = Predicate \number -> number `mod` 2 == 0
Now, if we want a Predicate
for strings that only accepts strings with an even length, we can use cmap
to adapt our existing evenNumberOnly
predicate:
lengthOfString :: String -> Int
lengthOfString = length
stringWithEvenLength :: Predicate String
stringWithEvenLength = cmap lengthOfString evenNumberOnly
Here, cmap
transforms the input type from Int
to String
by applying lengthOfString
, allowing our Predicate
to work with strings based on their lengths.
- Unlike
cmap
, which transforms the input that a structure processes,map
in a covariant context directly transforms the output produced by a structure
data Box a = Box a
map :: (a -> b) -> (Box a -> Box b)
If we have a function that converts Apple
to AppleJuice
, map
can be applied to transform a Box Apple
to a Box AppleJuice
.
In functional programming, the direction of transformation matters.
To reiterate: Contravariant functors, cmap
, adjust the input a structure accepts, while covariant functors, such as map
, modify the output produced by the structure. This distinction is key to grasping the precise difference between covariant and contravariant functors.