Functors, Applicative Functors and Monoids:
So far, when we were mapping functions over functors, we usually mapped functions that take only one parameter. But what happens when we map a function like
*, which takes two parameters, over a functor?
import cats._, cats.syntax.all._
{
val hs = Functor[List].map(List(1, 2, 3, 4)) ({(_: Int) * (_:Int)}.curried)
Functor[List].map(hs) {_(9)}
}
// res0: List[Int] = List(9, 18, 27, 36)
LYAHFGG:
But what if we have a functor value of
Just (3 *)and a functor value ofJust 5, and we want to take out the function fromJust(3 *)and map it overJust 5?Meet the
Applicativetypeclass. It lies in theControl.Applicativemodule and it defines two methods,pureand<*>.
Cats splits Applicative into Cartesian, Apply, and Applicative. Here’s the contract for Apply:
/**
* Weaker version of Applicative[F]; has apply but not pure.
*
* Must obey the laws defined in cats.laws.ApplyLaws.
*/
@typeclass(excludeParents = List("ApplyArityFunctions"))
trait Apply[F[_]] extends Functor[F] with Cartesian[F] with ApplyArityFunctions[F] { self =>
/**
* Given a value and a function in the Apply context, applies the
* function to the value.
*/
def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
....
}
Note that Apply extends Functor, Cartesian, and ApplyArityFunctions.
The <*> function is called ap in Cats’ Apply. (This was originally called apply, but was renamed to ap. +1)
LYAHFGG:
You can think of
<*>as a sort of a beefed-upfmap. Whereasfmaptakes a function and a functor and applies the function inside the functor value,<*>takes a functor that has a function in it and another functor and extracts that function from the first functor and then maps it over the second one.
LYAHFGG:
With the
Applicativetype class, we can chain the use of the<*>function, thus enabling us to seamlessly operate on several applicative values instead of just one.
Here’s an example in Haskell:
ghci> pure (-) <*> Just 3 <*> Just 5
Just (-2)
Cats comes with the apply syntax.
(3.some, 5.some) mapN { _ - _ }
// res1: Option[Int] = Some(value = -2)
(none[Int], 5.some) mapN { _ - _ }
// res2: Option[Int] = None
(3.some, none[Int]) mapN { _ - _ }
// res3: Option[Int] = None
This shows that Option forms Cartesian.
LYAHFGG:
Lists (actually the list type constructor,
[]) are applicative functors. What a surprise!
Let’s see if we can use the apply sytax:
(List("ha", "heh", "hmm"), List("?", "!", ".")) mapN {_ + _}
// res4: List[String] = List(
// "ha?",
// "ha!",
// "ha.",
// "heh?",
// "heh!",
// "heh.",
// "hmm?",
// "hmm!",
// "hmm."
// )
*> and <* operators Apply enables two operators, <* and *>, which are special cases of Apply[F].map2。
The definition looks simple enough, but the effect is cool:
1.some <* 2.some
// res5: Option[Int] = Some(value = 1)
none[Int] <* 2.some
// res6: Option[Int] = None
1.some *> 2.some
// res7: Option[Int] = Some(value = 2)
none[Int] *> 2.some
// res8: Option[Int] = None
If either side fails, we get None.
Before we move on, let’s look at the syntax that Cats adds to create an Option value.
9.some
// res9: Option[Int] = Some(value = 9)
none[Int]
// res10: Option[Int] = None
We can write (Some(9): Option[Int]) as 9.some.
Here’s how we can use it with Apply[Option].ap:
import cats._, cats.syntax.all._
Apply[Option].ap({{(_: Int) + 3}.some })(9.some)
// res12: Option[Int] = Some(value = 12)
Apply[Option].ap({{(_: Int) + 3}.some })(10.some)
// res13: Option[Int] = Some(value = 13)
Apply[Option].ap({{(_: String) + "hahah"}.some })(none[String])
// res14: Option[String] = None
Apply[Option].ap({ none[String => String] })("woot".some)
// res15: Option[String] = None
If either side fails, we get None.
If you remember Making our own typeclass with simulacrum from yesterday, simulacrum will automatically transpose the function defined on the typeclass contract into an operator, magically.
({(_: Int) + 3}.some) ap 9.some
// res16: Option[Int] = Some(value = 12)
({(_: Int) + 3}.some) ap 10.some
// res17: Option[Int] = Some(value = 13)
({(_: String) + "hahah"}.some) ap none[String]
// res18: Option[String] = None
(none[String => String]) ap "woot".some
// res19: Option[String] = None
LYAHFGG:
Control.Applicativedefines a function that’s calledliftA2, which has a type of
liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c .
Remember parameters are flipped around in Scala.
What we have is a function that takes F[B] and F[A], then a function (A, B) => C.
This is called map2 on Apply.
@typeclass(excludeParents = List("ApplyArityFunctions"))
trait Apply[F[_]] extends Functor[F] with Cartesian[F] with ApplyArityFunctions[F] { self =>
def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
def productR[A, B](fa: F[A])(fb: F[B]): F[B] =
map2(fa, fb)((_, b) => b)
def productL[A, B](fa: F[A])(fb: F[B]): F[A] =
map2(fa, fb)((a, _) => a)
override def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] =
ap(map(fa)(a => (b: B) => (a, b)))(fb)
/** Alias for [[ap]]. */
@inline final def <*>[A, B](ff: F[A => B])(fa: F[A]): F[B] =
ap(ff)(fa)
/** Alias for [[productR]]. */
@inline final def *>[A, B](fa: F[A])(fb: F[B]): F[B] =
productR(fa)(fb)
/** Alias for [[productL]]. */
@inline final def <*[A, B](fa: F[A])(fb: F[B]): F[A] =
productL(fa)(fb)
/**
* ap2 is a binary version of ap, defined in terms of ap.
*/
def ap2[A, B, Z](ff: F[(A, B) => Z])(fa: F[A], fb: F[B]): F[Z] =
map(product(fa, product(fb, ff))) { case (a, (b, f)) => f(a, b) }
def map2[A, B, Z](fa: F[A], fb: F[B])(f: (A, B) => Z): F[Z] =
map(product(fa, fb))(f.tupled)
def map2Eval[A, B, Z](fa: F[A], fb: Eval[F[B]])(f: (A, B) => Z): Eval[F[Z]] =
fb.map(fb => map2(fa, fb)(f))
....
}
For binary operators, map2 can be used to hide the applicative style.
Here we can write the same thing in two different ways:
(3.some, List(4).some) mapN { _ :: _ }
// res20: Option[List[Int]] = Some(value = List(3, 4))
Apply[Option].map2(3.some, List(4).some) { _ :: _ }
// res21: Option[List[Int]] = Some(value = List(3, 4))
The results match up.
The 2-parameter version of Apply[F].ap is called Apply[F].ap2:
Apply[Option].ap2({{ (_: Int) :: (_: List[Int]) }.some })(3.some, List(4).some)
// res22: Option[List[Int]] = Some(value = List(3, 4))
There’s a special case of map2 called tuple2, which works like this:
Apply[Option].tuple2(1.some, 2.some)
// res23: Option[(Int, Int)] = Some(value = (1, 2))
Apply[Option].tuple2(1.some, none[Int])
// res24: Option[(Int, Int)] = None
If you are wondering what happens when you have a function with more than two
parameters, note that Apply[F[_]] extends ApplyArityFunctions[F].
This is auto-generated code that defines ap3, map3, tuple3, … up to
ap22, map22, tuple22.
Apply has a single law called composition:
trait ApplyLaws[F[_]] extends FunctorLaws[F] {
implicit override def F: Apply[F]
def applyComposition[A, B, C](fa: F[A], fab: F[A => B], fbc: F[B => C]): IsEq[F[C]] = {
val compose: (B => C) => (A => B) => (A => C) = _.compose
fa.ap(fab).ap(fbc) <-> fa.ap(fab.ap(fbc.map(compose)))
}
}