When we say Functor f , we are saying that f is a covariant functor, which essentially means that for each function a → b , there is a corresponding function f a → f b that is the result of lifting a → b into f . The name “covariant”, with the co prefix this time meaning with or jointly, evokes the idea that as the a in f a varies through a → b , the entirety of f a varies to f b with it, in the same direction.

But as with many precious things in life, it’s hard to appreciate why this is important, or at all interesting, until we’ve lost it. So let’s lose it. Let’s look at this matter from the other side, from its dual, a dual we find by just flipping arrows. So let’s try that and see what happens.

contramap :: ( a -> b) -> (g a <- g b)

A covariant functor f , we said, was one that could lift a function a → b into a function f a → f b . So, presumably, the dual of said covariant functor f —let’s call it “ g the contravariant”— is one that lifts a function a → b into g a ← g b instead, arrow flipped. Conceptually, this is perfect. In practice, in Haskell, function arrows always go from left to right → , never from right to left ← , so we need to take care of that detail. Let’s write the function arrow in the direction it’s expected, flipping the position of the arguments instead.

contramap :: ( a -> b) -> (g b -> g a)

In Haskell, contramap exists as the sole method of the typeclass called Contravariant , which is like Functor but makes our minds bend.

class Contravariant (g :: Type -> Type) where contramap :: (a -> b) -> (g b -> g a)

Bend how? Why, pick a g and see. Maybe perhaps? Sure, that’s simple enough and worked for us before as a Functor .

contramap :: (a -> b) -> (Maybe b -> Maybe a)

Here, contramap is saying that given a way to convert a s into b s, we will be granted a tool for converting Maybe b s into Maybe a s. That is, a lifted function with the positions of its arguments flipped, exactly what we wanted. Let’s try to implement this by just following the types, as we’ve done many times before. Let’s write the Contravariant instance for Maybe .

instance Contravariant Maybe where contramap = \f yb -> case yb of Nothing -> Nothing Just b -> ☠

A pickle. We know that f is of type a → b , we know that yb is a Maybe b , and we know that we must return a value of type Maybe a somehow. Furthermore, we know that a and b could be anything, for they’ve been universally quantified. There is an implicit ∀ in there, always remember that. When yb is Nothing , we just return a new Nothing of the expected type Maybe a . And when yb is Just b ? Why, we die of course, for we need a way to turn that b into an a that we can put in a new Just , and we have none.

But couldn’t we just return Nothing ? It is a perfectly valid expression of type Maybe a , isn’t it? Well, kind of. It type-checks, sure, but a vestigial tingling sensation tells us it’s wrong. Or, well, maybe it doesn’t, but at least we have laws that should help us judge. So let’s use these laws to understand why this behavior would be characterized as evil, lest we hurt ourselves later on. Here is the simplified version of the broken instance we want to check, the one that always returns Nothing .

instance Contravariant Maybe where contramap = \_ _ -> Nothing

Like the Identity Law for fmap , the Identity Law for contramap says that contramap ping id over some value x should result in that same x .

contramap id x == x

A broken contramap for Maybe that always returns Nothing would blatantly violate this law. Applying contramap id (Just 5) , for example, would result in Nothing rather than Just 5 . This should be enough proof that ours would be a broken Contravariant instance, but for completeness, let’s take a look at the second contramap law as well.

Just like we have a Composition Law for fmap , we have one for contramap as well. It says that contramap ping the composition of two functions f and g over some value x should achieve the same as applying, to that x , the composition of the contramap ping of g with the contramap ping of f .

contramap (compose f g) x == compose (contramap g) (contramap f) x

Isn’t this the same as the Composition Law for fmap ? Nice try, but take a closer look.

fmap (compose f g) x == compose (fmap f) (fmap g) x contramap (compose f g) x == compose (contramap g) (contramap f) x

Whereas in the case of fmap , the order in which f and g appear at opposite sides of this equality is the same, it is not the same in the case of contramap . This shouldn’t come as a big surprise, seeing how we already knew that contramap gives us a lifted function with its arguments in the opposite position. Intuitively, dealing with Contravariant values means we are going to be composing things backwards. Or, should we say, forwards? After all, compose initially shocked us with its daring, so-called backwards sense of direction, and now we are mostly just backpedaling on it.

But the important question is whether our broken Contravariant instance for Maybe violates this law. And the answer is, unsurprisingly I hope, not at all. Both sides of this equality always result in Nothing , so they are indeed equal. Many times they will be equally wrong, but that doesn’t make them any less equal. And this is, technically, sufficient for our broken instance to satisfy this law. Thankfully we had that other law we could break.

So how do we contramap our Maybe s? Well, we don’t. As handy as it would be to have a magical way of going from b to a when all we know is going from a to b , we just can’t do that. Think how absurd it would be. If you need some creativity, just imagine a being a tree and b being fire. So, to sum up, Maybe s are covariant functors, as witnessed by the existence of their Functor instance, but they are not contravariant functors at all, as witnessed by the impossibility of coming up with a Contravariant instance for them. So once again, like in our previous Bifunctor conundrum, we find ourselves wanting for a function b → a when all we have is a function a → b . This time, however, we are prepared. We know that we keep ending up here because we are getting the position of our arguments, the variance of our functors, wrong. And we will fix that.