Contravariant

API Documentation: Contravariant

The Contravariant type class is for functors that define a contramap function with the following type:

def contramap[A, B](fa: F[A])(f: B => A): F[B]

It looks like regular (also called Covariant) Functor's map, but with the f transformation reversed.

Generally speaking, if you have some context F[A] for type A, and you can get an A value out of a B value — Contravariant allows you to get the F[B] context for B.

Examples of Contravariant instances are Show and scala.math.Ordering (along with cats.kernel.Order).

Contravariant instance for Show.

Say we have a class Money with a Show instance, and a Salary class:

import cats._

import cats.syntax.all._

case class Money(amount: Int)
case class Salary(size: Money)

implicit val showMoney: Show[Money] = Show.show(m => s"$$${m.amount}")

If we want to show a Salary instance, we can just convert it to a Money instance and show that instead.

Let's use Show's Contravariant:

implicit val showSalary: Show[Salary] = showMoney.contramap(_.size)
// showSalary: Show[Salary] = cats.Show$$anon$2$$Lambda$12566/0x00007f89bb27b928@4e72f49c

Salary(Money(1000)).show
// res0: String = "$1000"

Contravariant instance for scala.math.Ordering.

The Show example is trivial and quite far-fetched, let's see how Contravariant can help with orderings.

The scala.math.Ordering type class defines comparison operations, e.g. compare:

Ordering.Int.compare(2, 1)
// res1: Int = 1
Ordering.Int.compare(1, 2)
// res2: Int = -1

There's also a method, called by, that creates new Orderings out of existing ones:

def by[T, S](f: T => S)(implicit ord: Ordering[S]): Ordering[T]

In fact, it is just contramap, defined in a slightly different way! We supply T => S to receive F[S] => F[T] back.

So let's use it to our advantage and get Ordering[Money] for free:

// we need this for `<` to work
import scala.math.Ordered._

implicit val moneyOrdering: Ordering[Money] = Ordering.by(_.amount)
// moneyOrdering: Ordering[Money] = scala.math.Ordering$$anon$5@53e43d96

Money(100) < Money(200)
// res3: Boolean = true

Subtyping

Contravariant functors have a natural relationship with subtyping, dual to that of covariant functors:

class A
class B extends A
val b: B = new B
// b: B = repl.MdocSession$MdocApp$B@57e4acb7
val a: A = b
// a: A = repl.MdocSession$MdocApp$B@57e4acb7
val showA: Show[A] = Show.show(a => "a!")
// showA: Show[A] = cats.Show$$$Lambda$12565/0x00007f89bb27b4b0@5f791712
val showB1: Show[B] = showA.contramap(b => b: A)
// showB1: Show[B] = cats.Show$$anon$2$$Lambda$12566/0x00007f89bb27b928@6f6ce75
val showB2: Show[B] = showA.contramap(identity[A])
// showB2: Show[B] = cats.Show$$anon$2$$Lambda$12566/0x00007f89bb27b928@748c5bb0
val showB3: Show[B] = Contravariant[Show].narrow[A, B](showA)
// showB3: Show[B] = cats.Show$$$Lambda$12565/0x00007f89bb27b4b0@5f791712

Subtyping relationships are "lifted backwards" by contravariant functors, such that if F is a lawful contravariant functor and B <: A then F[A] <: F[B], which is expressed by Contravariant.narrow.