Contravariant Functors — An Intuition
Today I came across a blog post listing different types of functors (expressed using Scala), and once again I found a mention of contravariant functors. I heard about them in the past, saw their signature, but... no epiphany.
In this post I'd like to gain an intution for what a contravariant functor is. I'll leave it for another post to understand the other scary one, the exponential functor (shouldn't be much harder I presume, seems to be composed of two other functors — one covariant, the other contravariant).
Here's the signature given for contravariant functor in the above blog post:
trait Contravariant[F[_]] {
def contramap[A, B](f: B => A): F[A] => F[B]
}
It's pretty mind bending (for me) to understand how that f
function can possibly
get a handle over a value of type B
when B
can't be found in the argument list,
only in the return type (as opposed to A
). What's going on then?
A Concrete Example
After a brief search on the internet I found out that a good example of a
contravariant functor can actually be seen in the Scala standard library.
Ordering.by
is a contramap
incarnation, specialized for instances of
Ordering
.
def by[T, S](f: T => S)(implicit ord: Ordering[S]): Ordering[T]
The signature looks a bit different that the one used by Tony Morris, but they're
the same conceptually (the former is just more general). What it says is that if
you know how to compare elements of type S
and you have a function relating
elements of type T
to elements of type S
, then you know how to compare elements
of type T
.
There are two conceptual mappings here. At a higher-level you want to map
Ordering[S]
to Ordering[T]
, but in order to achieve that an opposite mapping
is required — from T
to S
— hence "contra".
Generalizing we get that contramap
is a function which says that if you give
it some abstraction over a concept A
, i.e., F[A]
, and a function that maps
from a different concept B
to the concept A
, then it is able to give you back
an abstraction over B
, i.e., F[B]
. Pretty abstract maybe, but bear with me.
Intuition
Here's a simple real world example inspired by this thread.
Everybody knows how to compare numbers, right? 2 is smaller than 3, 3 is greater than 2. Also, everybody knows what money are. It's a concept we've been using for ages now, and it's pretty trivial to draw a correspondence between money and numbers. We'd map a one dollar bill (100 cents) to the number 100 for example.
Because we know these two things: 1. how to compare numbers; 2. how to map from
money to numbers, we can actually compare money. Obvious, right? Well, calling
contramap
using those two facts as arguments will provide you the knowledge of
how to compare money. That's exactly the knowledge that a contravariant functor
encodes, but in a more general way, not only for numbers, money and comparisons.
As I said, the final goal is to actually map F[A] => F[B]
, but in order to
achieve that we need a function that maps B => A
, i.e. the opposite (contra)
direction. It's basic abstraction. You build new abstraction out of old ones by
specifying relationships between concepts.
Translating to Scala
Let's put the above example in Scala code. First, let's represent Money
:
case class Money(amount: Int)
Scala already knows how to compare Int
s and based on that knowledge we want to
specify how Money
should be compared, i.e., derive Ordering[Money]
from
Ordering[Int]
. To achieve that, we need a way to specify which sub-component
of Money
is our desired Int
, which is trivial in this example:
val contramapFn: Money => Int = (money) => money.amount
Having defined that, and given that an implicit instance for Ordering[Int]
is
already in scope, all we need now is to call the contramap
function defined on
the Ordering
object, namely, the by
method. The resulted Ordering[Money]
is marked as being implicit because we want the compiler to use it when we use
the comparison method <
.
implicit val moneyOrd: Ordering[Money] = Ordering.by(contramapFn)
Now we can easily compare Money
instances (we still need an implicit conversion
from Ordering
to Ordered
in scope, hence the first import):
scala> import scala.math.Ordered._
import scala.math.Ordered._
scala> Money(13) < Money(20)
res0: Boolean = true
scala> Money(23) < Money(20)
res1: Boolean = false
Implementing by
How is Ordering.by
implemented though? One possible implementation looks like
this (the real one is a bit more complicated due to optimizations):
def by[T, S](f: T => S)(implicit ord: Ordering[S]): Ordering[T] =
new Ordering[T] {
def compare(x:T, y:T) = ord.compare(f(x), f(y))
}
As you can see, it creates an Ordering
instance whose compare
method simply
delegates to the compare
method of the known Ordering[S]
, but not before
transforming the x
and y
parameters by passing them through f
.
This simple implementation also gives an idea of how f
is able to receive an
argument of type T
. It creates a new "function" that will receive T
s and
inside that "function" will we have access to these T
s to pass to f
. When you
need a value of a specific type and you don't have it, what do you do? You return
a function that asks for one. In this particular case we don't have a proper
function, but an object (an instance of Ordering
), which is nothing more than
a collection of functions, so the reasoning holds.
Conclusion
There are a few more concrete implementation of contravariant functors in the pages I've linked to in the resources section below, but the principle remains the same — you build new abstractions out of old ones by providing a mapping function from the new concept being abstracted to the old one that has already been abstracted.