Covariant vs Contravariant in Functional Programming

Saravanan M
4 min readMay 16, 2024

--

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.

Photo by Pablo García Saldaña on Unsplash

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 a Predicate 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.

--

--

Saravanan M

A place where I share my perspective on the tech topics I've read and enjoyed. Sometimes highly opinionated.