Contravariant Functors in TypeScript
Exploring the extended functor family part I
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:
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
:
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:
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:
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:
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 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.