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
Applicative
typeclass. It lies in theControl.Applicative
module and it defines two methods,pure
and<*>
.
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
. Whereasfmap
takes 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
Applicative
type 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.Applicative
defines 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)))
}
}