Apply
extends the Functor
type class (which features the familiar map
function) with a new function ap
. The ap
function is similar to map
in that we are transforming a value in a context (a context being the F
in F[A]
;
a context can be Option
, List
or Future
for example).
However, the difference between ap
and map
is that for ap
the function that
takes care of the transformation is of type F[A => B]
, whereas for map
it is A => B
:
Here are the implementations of Apply
for the Option
and List
types:
import cats._
implicit val optionApply: Apply[Option] = new Apply[Option] {
def ap[A, B](f: Option[A => B])(fa: Option[A]): Option[B] =
fa.flatMap(a => f.map(ff => ff(a)))
def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa map f
def product[A, B](fa: Option[A], fb: Option[B]): Option[(A, B)] =
fa.flatMap(a => fb.map(b => (a, b)))
}
implicit val listApply: Apply[List] = new Apply[List] {
def ap[A, B](f: List[A => B])(fa: List[A]): List[B] =
fa.flatMap(a => f.map(ff => ff(a)))
def map[A, B](fa: List[A])(f: A => B): List[B] = fa map f
def product[A, B](fa: List[A], fb: List[B]): List[(A, B)] =
fa.zip(fb)
}
Since Apply
extends Functor
, we can use the map
method from Functor
:
import cats.implicits._
val intToString: Int => String = _.toString
val double: Int => Int = _ * 2
val addTwo: Int => Int = _ + 2
Apply[Option].map(Some(1))(intToString) should be(res0)
Apply[Option].map(Some(1))(double) should be(res1)
Apply[Option].map(None)(addTwo) should be(res2)
And like functors, Apply
instances also compose:
val listOpt = Apply[List] compose Apply[Option]
val plusOne = (x: Int) => x + 1
listOpt.ap(List(Some(plusOne)))(List(Some(1), None, Some(3))) should be(res0)
The ap
method is a method that Functor
does not have:
Apply[Option].ap(Some(intToString))(Some(1)) should be(res0)
Apply[Option].ap(Some(double))(Some(1)) should be(res1)
Apply[Option].ap(Some(double))(None) should be(res2)
Apply[Option].ap(None)(Some(1)) should be(res3)
Apply[Option].ap(None)(None) should be(res4)
Apply
also offers variants of ap
. The functions apN
(for N
between 2
and 22
)
accept N
arguments where ap
accepts 1
.
Note that if any of the arguments of this example is None
, the
final result is None
as well. The effects of the context we are operating on
are carried through the entire computation:
val addArity2 = (a: Int, b: Int) => a + b
Apply[Option].ap2(Some(addArity2))(Some(1), Some(2)) should be(res0)
Apply[Option].ap2(Some(addArity2))(Some(1), None) should be(res1)
val addArity3 = (a: Int, b: Int, c: Int) => a + b + c
Apply[Option].ap3(Some(addArity3))(Some(1), Some(2), Some(3)) should be(res2)
Similarly, mapN
functions are available:
Apply[Option].map2(Some(1), Some(2))(addArity2) should be(res0)
Apply[Option].map3(Some(1), Some(2), Some(3))(addArity3) should be(res1)
Similarly, tupleN
functions are available:
Apply[Option].tuple2(Some(1), Some(2)) should be(res0)
Apply[Option].tuple3(Some(1), Some(2), Some(3)) should be(res1)
In order to use functions apN
, mapN
and tupleN
*infix*,
import cats.implicits._
.
import cats.implicits._
val option2 = (Option(1), Option(2))
val option3 = (option2._1, option2._2, Option.empty[Int])
option2 mapN addArity2 should be(res0)
option3 mapN addArity3 should be(res1)
option2 apWith Some(addArity2) should be(res2)
option3 apWith Some(addArity3) should be(res3)
option2.tupled should be(res4)
option3.tupled should be(res5)