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 class Money with a Show instance, and Salary class.

import cats._
import cats.functor._
import cats.implicits._

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 it instead.

Let’s use Show’s Contravariant:

implicit val showSalary: Show[Salary] = showMoney.contramap(_.size)
// showSalary: cats.Show[Salary] = cats.Show$$anon$1@757cc560

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

Contravariant instance for scala.math.Ordering.

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

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

Ordering.Int.compare(2, 1)
// res3: Int = 1

Ordering.Int.compare(1, 2)
// res4: 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 in our advantage and get Ordering[Money] for free:

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

implicit val moneyOrdering: Ordering[Money] = Ordering.by(_.amount)
// moneyOrdering: Ordering[Money] = scala.math.Ordering$$anon$10@165a2acf

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

Subtyping

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

class A
// defined class A

class B extends A
// defined class B

val b: B = new B
// b: B = B@49078c11

val a: A = b
// a: A = B@49078c11

val showA: Show[A] = Show.show(a => "a!")
// showA: cats.Show[A] = cats.Show$$anon$1@31fe1b35

val showB1: Show[B] = showA.contramap(b => b: A)
// showB1: cats.Show[B] = cats.Show$$anon$1@58d10ebb

val showB2: Show[B] = showA.contramap(identity[A])
// showB2: cats.Show[B] = cats.Show$$anon$1@1d1f05fa

val showB3: Show[B] = Contravariant[Show].narrow[A, B](showA)
// showB3: cats.Show[B] = cats.Show$$anon$1@31fe1b35

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