Functor

1 minute read

A non-mathematical introduction to functors.

Option and List are two well-known classes, both of which have the function map that takes a function that is applied to the values contained by the List or Option.

Here are two functions that take List[Int] and Option[Int] and increments the values:

def inc(o: Option[Int]): Option[Int] = o.map(i => i + 1)

def inc(l: List[Int]): List[Int] = l.map(i => i + 1)

The inc functions do not care about Option or List particularly, they need a way to work on the values they contain, but sadly inc is duplicated. It doesn’t have to be.

One way to fix this is to use a Functor that supplies a map function for any F[_].

// scala 3
trait Functor[F[_]] {
  extension [A, B](x: F[A])
    def map(f: A => B): F[B]
}

Given x: F[A], Functor[F] provides a map method. Here is inc written using Functor[F]:

def inc[F[_]: Functor](a: F[Int]): F[Int] = a.map(i => i + 1)

F[_]: Functor means that it requires an instance of Functor[F]. Scala finds that instance by looking in lots of places for it, one being the companion object of the F[_].

For Option and List those instances are:

given Functor[Option] with {
  extension [A, B](m: Option[A])
    override def map(f: A => B): Option[B] = m.map(f)
}

given Functor[List] with {
  extension [A, B](m: List[A])
    override def map(f: A => B): List[B] = m.map(f)
}

Your own classes can play too:

final case class Blub[A](v: A)

object Blub {
  given Functor[Blub] with {
    extension [A, B](blub: Blub[A])
      override def map(f: A => B): Blub[B]
        = Blub(f(blub.v))
  }
}

inc(Blub(1)) gives Blub(2)

Note that this is an example of the typeclass pattern, read more here.

Laws

A proper functor must obey two laws:

  1. Identity: Mapping with the identity function is a no-op
  2. Composition: fa.map(f).map(g) = fa.map(f.andThen(g)

Read more