A Functor
is a ubiquitous type class involving types that have one
"hole", i.e. types which have the shape F[*]
, such as Option
,
List
and Future
. (This is in contrast to a type like Int
which has
no hole, or Tuple2
which has two holes (Tuple2[*,*]
)).
The Functor
category involves a single operation, named map
:
def map[A, B](fa: F[A])(f: A => B): F[B]
This method takes a function A => B
and turns an F[A]
into an
F[B]
. The name of the method map
should remind you of the map
method that exists on many classes in the Scala standard library, for
example:
Option(1).map(_ + 1)
List(1, 2, 3).map(_ + 1)
Vector(1, 2, 3).map(_.toString)
We can trivially create a Functor
instance for a type which has a well
behaved map
method:
import cats._
implicit val optionFunctor: Functor[Option] = new Functor[Option] {
def map[A, B](fa: Option[A])(f: A => B) = fa map f
}
implicit val listFunctor: Functor[List] = new Functor[List] {
def map[A, B](fa: List[A])(f: A => B) = fa map f
}
However, functors can also be created for types which don't have a map
method. For example, if we create a Functor
for Function1[In, *]
we can use andThen
to implement map
:
implicit def function1Functor[In]: Functor[Function1[In, *]] =
new Functor[Function1[In, *]] {
def map[A, B](fa: In => A)(f: A => B): Function1[In, B] = fa andThen f
}
This example demonstrates the use of the
kind-projector compiler plugin
This compiler plugin can help us when we need to change the number of type
holes. In the example above, we took a type which normally has two type holes,
Function1[*,*]
and constrained one of the holes to be the In
type,
leaving just one hole for the return type, resulting in Function1[In,*]
.
Without kind-projector, we'd have to write this as something like
({type F[A] = Function1[In,A]})#F
, which is much harder to read and understand.
List
is a functor which applies the function to each element of the list:
Functor[List].map(List("qwer", "adsfg"))(_.length)
Option
is a functor which only applies the function when the Option
value
is a Some
:
Functor[Option].map(Option("Hello"))(_.length) should be(res0)
Functor[Option].map(None: Option[String])(_.length) should be(res1)
We can use Functor
to "lift" a function from A => B
to F[A] => F[B]
:
val lenOption: Option[String] => Option[Int] = Functor[Option].lift(_.length)
lenOption(Some("abcd"))
We can now apply the lenOption
function to Option
instances.
val lenOption: Option[String] => Option[Int] = Functor[Option].lift(_.length)
lenOption(Some("Hello")) should be(res0)
Functor
provides an fproduct
function which pairs a value with the
result of applying a function to that value.
val source = List("Cats", "is", "awesome")
val product = Functor[List].fproduct(source)(_.length).toMap
product.get("Cats").getOrElse(0) should be(res0)
product.get("is").getOrElse(0) should be(res1)
product.get("awesome").getOrElse(0) should be(res2)
Functors compose! Given any functor F[_]
and any functor G[_]
we can
create a new functor F[G[_]]
by composing them:
val listOpt = Functor[List] compose Functor[Option]
In the previous example the resulting functor will apply the map
operation through the two
type constructors: List
and Option
.
val listOpt = Functor[List] compose Functor[Option]
listOpt.map(List(Some(1), None, Some(3)))(_ + 1) should be(res0)