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
