Contravariant Functors in TypeScript

Exploring the extended functor family part I

Wim Jongeneel
Hoppinger
4 min readJun 11, 2020

--

Almost every programmer these days has at least heard of functors and monads. Understanding them is often seen as a sign of mastering functional programming. And while they are among the most powerful and versatile abstractions in functional programming, there are many more similar abstractions then just those two. In this series of articles we will tour a variety of lesser-known abstractions and see how they can deal with problems your regular functors and monads just can’t.

Those aren’t the abstractions that get coded in as much as functors and monads, but knowing and understanding them will help you reason about problems in a functional way. Training your brain to think in abstractions that are based on formal logic will make you a better (functional) programmer. If you recognize that your problem looks like one of the abstractions we are about to discuss then you also immediately know which options you have for solving it. And as an additional plus you can sound really smart if you know all the the magic words.

An impossible functor

As you might know, a function forms a functor when we fix the type arguments of the parameters. This one usually goes by the name of Reader and can be defined like this:

Reader functor in TypeScript

But what if we fix the return type and make the argument type the ‘a’ of our functor? We can define the type quite easily and it looks very similar to any of the functors with two type parameters like Reader, Result or Either:

ReversedReader is a functor?

This is a very generic definition that has many concrete instances. Most of them are functions that tell something about ‘a thing’ like predicates, sorting or equality. Some examples of types that follow this pattern are shown bellow:

Practical examples of our ReversedReader

This are all types that have a single type argument that look a lot like functors. But can we define a fmap method? The answer is no. You will need to define a function that goes from b to r and you only have two functions from a. There is literally nothing you can do with the functions and values you have.

Because of this it turn out that is is impossible to fmap things that depend on an a instead of being a box that contains an a. (The box-analogy is actual better then it often gets credits for.) To fully understand why this type can’t be defined as a functor we will first have to dive in some type theory about the different kinds of generic types.

Positive and negative generics

It the previous paragraph we saw that there is an important difference between generic type arguments a type depends on and type arguments a type contains. This is called a negative or a positive position of the type. Negative type positions popup around function arguments. Their type ‘has’ a type a, but the values don’t contain an a. This can be further clarified with some examples of negative and positive positions:

Positive and negative positions for type arguments

Our beloved ‘regular’ functor is a type that has one argument in a positive position. Another terminology for this is to say that a functor has a covariant type argument. A type argument in negative position is called a contravariant argument. When the type argument of a functor appearances in a negative position it is a contravariant functor (usually shortened to contravariant) as opposed to the regular covariant functor.

There is also a third option for type arguments that appear in both a positive and a negative position. This is special case that I will comeback to later down the road.

Defining the contravariant

As we have seen there is a functor-like abstraction that resolves around having a contravariant type argument. This functor can’t be mapped in the traditional way with a function from a to b, but we can map it with a function from b to a. This is function is called the contramap or cmap. An interface for this can be defined like this:

The contravariant interface

From now on in this series I will use fmap to reference to the mapping of a covariant functor to prevent confusion with cmap and other map functions to come. I can show you that this abstraction holds up by implementing it for some of the examples of contravariants we saw earlier in this article:

Contravariant definitions for the earlier examples

Contravariant laws

Like all interesting things in functional programming, the contravariant has some laws that you need to obey before you can call something a contravariant. Those laws are very similar to the functor laws as they are based on the same logic. The first one is the identity law which is exactly the same for cmap as for fmap:

c.cmap(id) == id(c) == c

This law ensures that cmap only maps the item in the functor and nothing else. The other one is the composition law. Just like covariant functors, contravariants have to respect composition when mapped. The main difference here is that f is a function from b to a and not a function from a to b. Likewise, g is a function from c to b instead of being a function from b to c like it would be for the composition law of fmap.

c.cmap(f).cmap(g) == c.cmap(compose(f, g))

compose is here a function that composes g after f, defined in TypeScript like this:

Conclusion

In this article I have shown you that there are more functors that just your everyday covariant functor. We have also looked at the different kind of generic type arguments, something that will keep on coming back when about all the different kinds of functors. In the next installments of this series I will introduce you to other members of the functor family.

--

--

Wim Jongeneel
Hoppinger

Software Engineer at Mendix (Rotterdam, The Netherlands) • Student MSc Software Engineering • Functional programming enthusiast