Exploring Contravariant Functors

Using Dart to Explore Contravariant Functors

September 1, 2019

Contravariant Functors are functors that are contravarient in their mapping function. In another post I noted that a functor is something that can be mapped over. Something impementing a law-abiding map method that maintains identity and composition. Cotravariant Functors are very similar to Functors, but are not as general. Contravariant Functors only make sense for types that represent transformations. That is, types that are not just wrappers around values like an List, Set, Map, or Options type.

For our exploration of Contravariant Functors we’ll be using a Show type that knows how to transforma given input to a String. We can then explore how Protofunctor may be useful.

Defining Print and the Protofunctor Interface

To start our print class is a wrapper around a function that convers from some type to a string.

typedef ShowFn<A> = String Function(A a);
class Show<A>{
  ShowFn<A> showFn;
  String show(A instance) => showFn(instance);

This Show interface can then be used as:

class Person { .... }
class PersonShow extends Show<Person> {
  PersonShow(): super((Person person) => "${person.first} ${person.last}");

new PersonShow().show(new Person("Alice", "Wonderland")); 

So now we have defined an interface that encapsulates a transformation, we want to start thinking about Contravariant Functors. Contravariant Functors are similar to Functors, with the exception that they are contravariant in the parameter that the corresponding map method recieves, and so commonly the defining method of Profunctos is named contramap. Below are the definitions along side that of Functor.

abstract class Functor<F, A> {
  F map<B>(B f(A _)); 

abstract class ContravariantFunctor<F, A> {
  F contramap<B>(A f(B _));

contramap is given a contravariant function, and is expected to accept and produce the same types as map. Let’s try implementing this for our new class and see what it might look like.

class PersonShow extends Show<Person> with ContravariantFunctor<Show, Person> {
  // ... override showFn same as above
  Show<Person> contramap<B>(Person f(B _)) {
    return new Show<B>((B b) => new PersonShow().show(f(b)));

Note that the only way for us to use the function is to accept a B and convert it to a an A more conretely if we accept a function that can convert to a Person, then we can reuse the PersonShow to that other type. The contramap method is therefor returning a new Show instance that reuses our PersonShow.

val robotToPerson = (Robot robot) => new Person(robot.alias, robot.serialNumber)
val robotShow = new PersonShow().contramap(robotToPerson);
robotShow(new Robot("Bender", "R0DR1GU3Z"));

Contravariant Functors at their core are prepending a function onto a transformation. In this case we are prepending the a transformation from Robot to Person, but we could chain as many transformations as we would like. The other key here is that at each step we have an instance of Show which can be consumed by other functions. Protofunctors much like functors are very much about coposability with the goal of reusing known consumers. In this case we are reusing Person’s implementation of Show.

Additional Reading