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)