Functor
API Documentation: Functor
Functor is a type class that abstracts over type constructors that can be map'ed over. Examples of such
type constructors are List, Option, and Future.
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
// Example implementation for Option
implicit val functorForOption: Functor[Option] = new Functor[Option] {
def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa match {
case None => None
case Some(a) => Some(f(a))
}
}
A Functor instance must obey two laws:
-
Composition: Mapping with
fand then again withgis the same as mapping once with the composition offandgfa.map(f).map(g) = fa.map(f.andThen(g))
-
Identity: Mapping with the identity function is a no-op
fa.map(x => x) = fa
A different view
Another way of viewing a Functor[F] is that F allows the lifting of a pure function A => B into the effectful
function F[A] => F[B]. We can see this if we re-order the map signature above.
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
def lift[A, B](f: A => B): F[A] => F[B] =
fa => map(fa)(f)
}
Functors for effect management
The F in Functor is often referred to as an "effect" or "computational context." Different effects will
abstract away different behaviors with respect to fundamental functions like map. For instance, Option's effect
abstracts away potentially missing values, where map applies the function only in the Some case but
otherwise threads the None through.
Taking this view, we can view Functor as the ability to work with a single effect - we can apply a pure
function to a single effectful value without needing to "leave" the effect.
Functors compose
If you've ever found yourself working with nested data types such as Option[List[A]] or
List[Either[String, Future[A]]] and tried to map over it, you've most likely found yourself doing something
like _.map(_.map(_.map(f))). As it turns out, Functors compose, which means if F and G have
Functor instances, then so does F[G[_]].
Such composition can be achieved via the Functor#compose method.
import cats.Functor
import cats.syntax.all._
val listOption = List(Some(1), None, Some(2))
// listOption: List[Option[Int]] = List(Some(value = 1), None, Some(value = 2))
// Through Functor#compose
Functor[List].compose[Option].map(listOption)(_ + 1)
// res1: List[Option[Int]] = List(Some(value = 2), None, Some(value = 3))
This approach will allow us to use composition without wrapping the value in question, but can
introduce complications in more complex use cases. For example, if we need to call another function which
requires a Functor and we want to use the composed Functor, we would have to explicitly pass in the
composed instance during the function call or create a local implicit.
def needsFunctor[F[_]: Functor, A](fa: F[A]): F[Unit] = Functor[F].map(fa)(_ => ())
def foo: List[Option[Unit]] = {
val listOptionFunctor = Functor[List].compose[Option]
type ListOption[A] = List[Option[A]]
needsFunctor[ListOption, Int](listOption)(listOptionFunctor)
}
We can make this nicer at the cost of boxing with the Nested data type.
import cats.data.Nested
import cats.syntax.all._
val nested: Nested[List, Option, Int] = Nested(listOption)
// nested: Nested[List, Option, Int] = Nested(
// value = List(Some(value = 1), None, Some(value = 2))
// )
nested.map(_ + 1)
// res2: Nested[List, Option, Int] = Nested(
// value = List(Some(value = 2), None, Some(value = 3))
// )
The Nested approach, being a distinct type from its constituents, will resolve the usual way modulo
possible SI-2712 issues (which can be addressed through partial unification),
but requires syntactic and runtime overhead from wrapping and unwrapping.