Apply
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:
import cats._
val intToString: Int => String = _.toString
val double: Int => Int = _ * 2
val addTwo: Int => Int = _ + 2
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
}
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
}
map
Since Apply extends Functor, we can use the map method from Functor:
Apply[Option].map(Some(1))(intToString)
// res3: Option[String] = Some(1)
Apply[Option].map(Some(1))(double)
// res4: Option[Int] = Some(2)
Apply[Option].map(None)(double)
// res5: Option[Int] = None
compose
And like functors, Apply instances also compose (via the Nested data type):
import cats.data.Nested
// import cats.data.Nested
val listOpt = Nested[List, Option, Int](List(Some(1), None, Some(3)))
// listOpt: cats.data.Nested[List,Option,Int] = Nested(List(Some(1), None, Some(3)))
val plusOne = (x:Int) => x + 1
// plusOne: Int => Int = <function1>
val f = Nested[List, Option, Int => Int](List(Some(plusOne)))
// f: cats.data.Nested[List,Option,Int => Int] = Nested(List(Some(<function1>)))
Apply[Nested[List, Option, ?]].ap(f)(listOpt)
// res6: cats.data.Nested[[+A]List[A],Option,Int] = Nested(List(Some(2), None, Some(4)))
ap
The ap method is a method that Functor does not have:
Apply[Option].ap(Some(intToString))(Some(1))
// res7: Option[String] = Some(1)
Apply[Option].ap(Some(double))(Some(1))
// res8: Option[Int] = Some(2)
Apply[Option].ap(Some(double))(None)
// res9: Option[Int] = None
Apply[Option].ap(None)(Some(1))
// res10: Option[Nothing] = None
Apply[Option].ap(None)(None)
// res11: Option[Nothing] = None
ap2, ap3, etc
Apply also offers variants of ap. The functions apN (for N between 2 and 22)
accept N arguments where ap accepts 1:
For example:
val addArity2 = (a: Int, b: Int) => a + b
// addArity2: (Int, Int) => Int = <function2>
Apply[Option].ap2(Some(addArity2))(Some(1), Some(2))
// res12: Option[Int] = Some(3)
val addArity3 = (a: Int, b: Int, c: Int) => a + b + c
// addArity3: (Int, Int, Int) => Int = <function3>
Apply[Option].ap3(Some(addArity3))(Some(1), Some(2), Some(3))
// res13: Option[Int] = Some(6)
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:
Apply[Option].ap2(Some(addArity2))(Some(1), None)
// res14: Option[Int] = None
Apply[Option].ap4(None)(Some(1), Some(2), Some(3), Some(4))
// res15: Option[Nothing] = None
map2, map3, etc
Similarly, mapN functions are available:
Apply[Option].map2(Some(1), Some(2))(addArity2)
// res16: Option[Int] = Some(3)
Apply[Option].map3(Some(1), Some(2), Some(3))(addArity3)
// res17: Option[Int] = Some(6)
tuple2, tuple3, etc
And tupleN:
Apply[Option].tuple2(Some(1), Some(2))
// res18: Option[(Int, Int)] = Some((1,2))
Apply[Option].tuple3(Some(1), Some(2), Some(3))
// res19: Option[(Int, Int, Int)] = Some((1,2,3))
apply builder syntax
The |@| operator offers an alternative syntax for the higher-arity Apply
functions (apN, mapN and tupleN).
In order to use it, first import cats.syntax.all._ or cats.syntax.cartesian._.
Here we see that the following two functions, f1 and f2, are equivalent:
import cats.implicits._
// import cats.implicits._
def f1(a: Option[Int], b: Option[Int], c: Option[Int]) =
(a |@| b |@| c) map { _ * _ * _ }
// f1: (a: Option[Int], b: Option[Int], c: Option[Int])Option[Int]
def f2(a: Option[Int], b: Option[Int], c: Option[Int]) =
Apply[Option].map3(a, b, c)(_ * _ * _)
// f2: (a: Option[Int], b: Option[Int], c: Option[Int])Option[Int]
f1(Some(1), Some(2), Some(3))
// res20: Option[Int] = Some(6)
f2(Some(1), Some(2), Some(3))
// res21: Option[Int] = Some(6)
All instances created by |@| have map, ap, and tupled methods of the appropriate arity:
val option2 = Option(1) |@| Option(2)
// option2: cats.syntax.CartesianBuilder[Option]#CartesianBuilder2[Int,Int] = cats.syntax.CartesianBuilder$CartesianBuilder2@b01b8ad
val option3 = option2 |@| Option.empty[Int]
// option3: cats.syntax.CartesianBuilder[Option]#CartesianBuilder3[Int,Int,Int] = cats.syntax.CartesianBuilder$CartesianBuilder3@1d2cd142
option2 map addArity2
// res22: Option[Int] = Some(3)
option3 map addArity3
// res23: Option[Int] = None
option2 apWith Some(addArity2)
// res24: Option[Int] = Some(3)
option3 apWith Some(addArity3)
// res25: Option[Int] = None
option2.tupled
// res26: Option[(Int, Int)] = Some((1,2))
option3.tupled
// res27: Option[(Int, Int, Int)] = None