Understanding Contramap

Views
Article hero image

Functor, or covariant functor, is a typeclass that defines the map operation, which takes a F[A] and a transformer function A => B to return a F[B].

trait Functor[F[_]]:
  def map[A, B](fa: F[A])(f: A => B): F[B]

A contramap is defined as the converse of the map. I have never been satisfied with the definition. For starters, it is not defined in a Functor. So, where does contramap appear? It appears in a contravariant functor1 (Contravariant in cats).

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

The definition of contramap using converse is used mainly because of the direction of the arrow (A => B vs B => A). It is not wrong but it is not easy to understand and explain.

A common example used for contramap is the Encoder such as when using circe.

import io.circe._
import io.circe.syntax._

class Person(val name: String, val age: Int)

object Person:
  implicit val encoder: Encoder[Person] = (p: Person) =>
    Json.obj(
      ("name", Json.fromString(p.name)),
      ("age", Json.fromInt(p.age))
    )

class Employee(
  override val name: String,
  override val age: Int,
  val employeeId: String
) extends Person(name, age)

object Employee:
  implicit val encoder: Encoder[Employee] = Person.encoder.contramap(identity)

@main
def runEmployee(): Unit = {
  val employee = Employee("John Doe", 30, "E123")
  val employeeJson = employee.asJson
  println(employeeJson)
}

Reading the code above, it is not easy to explain what is going on with contramap.

In this post, I will explain the concept in a way that is easy to internalize.

Here is a motivating example:

trait Validator[-A]:
  def validate(a: A): Boolean

  // Defining a new Validator[B] using Validator[A] given a function that converts B to A.
  // 1. convert type B to type A using f
  // 2. validate the value of type A using Validator[A]
  def contramap[B](f: B => A): Validator[B] = (b: B) => validate(f(b))

  def &&[B <: A] (that: Validator[B]): Validator[B] =
    (value: B) => this.validate(value) && that.validate(value)

object Validator:
  def validate[A: Validator](value: A): Boolean =
    summon[Validator[A]].validate(value)
import User.{Name, Age}

case class User(name: Name, age: Age)
object User:
  case class Name(value: String)
  object Name:
    given Validator[Name] = _.value.nonEmpty

  case class Age(value: Int)
  object Age:
    given Validator[Age] = _.value > 0

  given Validator[User] =
    summon[Validator[Name]].contramap[User](_.name) &&
      summon[Validator[Age]].contramap[User](_.age)

@main
def runValidator(): Unit = {
  val user1 = User(Name("Alice"), Age(30))
  println(Validator.validate(user1)) // Output: true (valid user)

  val user2 = User(Name(""), Age(25)) // Invalid name
  println(Validator.validate(user2)) // Output: false (invalid user)

  val user3 = User(Name("Bob"), Age(-5)) // Invalid age
  println(Validator.validate(user3)) // Output: false (invalid user)
}

We have the validators for name and age (consider it Validator[A]). Now, we are defining Validator[User]2 (consider it Validator[B]) using the aforementioned validators, which is where contramap comes into play.

Contramap allows you to transform a function by changing the input type without changing the operation itself. In other words, it takes a function that works on one type and adapts it to work with another type by transforming the input, without changing the underlying operation.

A point to note is that contramap is relevant only when contravariance is in the picture which is why a Functor does not define it. That means only typeclasses taking parameters that are consumed rather than produced.

contramap is not well understood because it does not appear in abundance unlike our beloved map. Hope this post makes it easy to internalize contramap.


  1. There are other types of Functors - Invariant, Applicative, Bifunctor, etc. They are not relevant in this post. ↩︎

  2. Thanks to Morgen Peschke for perfecting the Validator example, especially using the summon[Validator[A]], which makes the API more natural. ↩︎

scala contramap functor fp