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$15388/0x00007f63309f5210@5045fb72
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@7da963f1
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@2e6ceb8a
val a: A = b
// a: A = repl.MdocSession$MdocApp$B@2e6ceb8a
val showA: Show[A] = Show.show(a => "a!")
// showA: Show[A] = cats.Show$$$Lambda$15387/0x00007f63309f4d98@1abcc77a
val showB1: Show[B] = showA.contramap(b => b: A)
// showB1: Show[B] = cats.Show$$anon$2$$Lambda$15388/0x00007f63309f5210@394d0914
val showB2: Show[B] = showA.contramap(identity[A])
// showB2: Show[B] = cats.Show$$anon$2$$Lambda$15388/0x00007f63309f5210@1ad64254
val showB3: Show[B] = Contravariant[Show].narrow[A, B](showA)
// showB3: Show[B] = cats.Show$$$Lambda$15387/0x00007f63309f4d98@1abcc77a
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
.