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
:
scala> import cats._
import cats._
scala> val intToString: Int => String = _.toString
intToString: Int => String = <function1>
scala> val double: Int => Int = _ * 2
double: Int => Int = <function1>
scala> val addTwo: Int => Int = _ + 2
addTwo: Int => Int = <function1>
scala> implicit val optionApply: Apply[Option] = new Apply[Option] {
| def ap[A, B](fa: Option[A])(f: Option[A => B]): 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
| }
optionApply: cats.Apply[Option] = $anon$1@290cf639
scala> implicit val listApply: Apply[List] = new Apply[List] {
| def ap[A, B](fa: List[A])(f: List[A => B]): 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
| }
listApply: cats.Apply[List] = $anon$1@2a1dcbd0
map
Since Apply
extends Functor
, we can use the map
method from Functor
:
scala> Apply[Option].map(Some(1))(intToString)
res0: Option[String] = Some(1)
scala> Apply[Option].map(Some(1))(double)
res1: Option[Int] = Some(2)
scala> Apply[Option].map(None)(double)
res2: Option[Int] = None
compose
And like functors, Apply
instances also compose:
scala> val listOpt = Apply[List] compose Apply[Option]
listOpt: cats.Apply[[X]List[Option[X]]] = cats.Apply$$anon$1@1fbd3054
scala> val plusOne = (x:Int) => x + 1
plusOne: Int => Int = <function1>
scala> listOpt.ap(List(Some(1), None, Some(3)))(List(Some(plusOne)))
res3: List[Option[Int]] = List(Some(2), None, Some(4))
ap
The ap
method is a method that Functor
does not have:
scala> Apply[Option].ap(Some(1))(Some(intToString))
res4: Option[String] = Some(1)
scala> Apply[Option].ap(Some(1))(Some(double))
res5: Option[Int] = Some(2)
scala> Apply[Option].ap(None)(Some(double))
res6: Option[Int] = None
scala> Apply[Option].ap(Some(1))(None)
res7: Option[Nothing] = None
scala> Apply[Option].ap(None)(None)
res8: 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:
scala> val addArity2 = (a: Int, b: Int) => a + b
addArity2: (Int, Int) => Int = <function2>
scala> Apply[Option].ap2(Some(1), Some(2))(Some(addArity2))
res9: Option[Int] = Some(3)
scala> val addArity3 = (a: Int, b: Int, c: Int) => a + b + c
addArity3: (Int, Int, Int) => Int = <function3>
scala> Apply[Option].ap3(Some(1), Some(2), Some(3))(Some(addArity3))
res10: 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:
scala> Apply[Option].ap2(Some(1), None)(Some(addArity2))
res11: Option[Int] = None
scala> Apply[Option].ap4(Some(1), Some(2), Some(3), Some(4))(None)
res12: Option[Nothing] = None
map2, map3, etc
Similarly, mapN
functions are available:
scala> Apply[Option].map2(Some(1), Some(2))(addArity2)
res13: Option[Int] = Some(3)
scala> Apply[Option].map3(Some(1), Some(2), Some(3))(addArity3)
res14: Option[Int] = Some(6)
tuple2, tuple3, etc
And tupleN
:
scala> Apply[Option].tuple2(Some(1), Some(2))
res15: Option[(Int, Int)] = Some((1,2))
scala> Apply[Option].tuple3(Some(1), Some(2), Some(3))
res16: 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.apply._
.
Here we see that the following two functions, f1
and f2
, are equivalent:
scala> import cats.syntax.apply._
import cats.syntax.apply._
scala> 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]
scala> 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]
scala> f1(Some(1), Some(2), Some(3))
res17: Option[Int] = Some(6)
scala> f2(Some(1), Some(2), Some(3))
res18: Option[Int] = Some(6)
All instances created by |@|
have map
, ap
, and tupled
methods of the appropriate arity:
scala> import cats.syntax.apply._
import cats.syntax.apply._
scala> val option2 = Option(1) |@| Option(2)
option2: cats.syntax.ApplyBuilder[Option]#ApplyBuilder2[Int,Int] = cats.syntax.ApplyBuilder$ApplyBuilder2@280020a2
scala> val option3 = option2 |@| Option.empty[Int]
option3: cats.syntax.ApplyBuilder[Option]#ApplyBuilder3[Int,Int,Int] = cats.syntax.ApplyBuilder$ApplyBuilder3@bdf8d4c
scala> option2 map addArity2
res19: Option[Int] = Some(3)
scala> option3 map addArity3
res20: Option[Int] = None
scala> option2 ap Some(addArity2)
res21: Option[Int] = Some(3)
scala> option3 ap Some(addArity3)
res22: Option[Int] = None
scala> option2.tupled
res23: Option[(Int, Int)] = Some((1,2))
scala> option3.tupled
res24: Option[(Int, Int, Int)] = None