Functor
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:
- Identity: Mapping with the identity function is a no-op
- Composition:
fa.map(f).map(g) = fa.map(f.andThen(g)