Explore “herding cats”: Semigroupal, Apply, Applicative

5 minute read


trait Semigroupal[F[_]] extends Serializable {
  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]

Here we’re dealing with cartesian product.

Semigroupal[Option].product(1.some, 2.some) === (1,2).some
Semigroupal[Option].product(1.some, none[Int]) === none[(Int, Int)]
Semigroupal[List].product(List(1, 2, 3), List("foo", "bar")) === List((1, "foo"), (1, "bar"), (2, "foo"), (2, "bar"), (3, "foo"), (3, "bar"))


Apply is quite tricky.

There are plenty of explanations that aren’t bringing much sense to me.

Docs describe main function def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] as “Given a value and a function in the Apply context, applies the function to the value.”

That still far away from motivation since our usual friends like List or Option typically shouldn’t handle function inside.

More or less good wording sounds like “apply lifts function A => B to container F[_]”.

It’s easy to construct such kind of composition where we build Option[A => B] and pass values there but that’s not a tooling for everyday anyway.

What is ap provides another one example.

val applyOption: Apply[Option] = Apply[Option]
val optionOfStringToUpperCase: Option[String] => Option[String] = applyOption.ap[String, String](((s: String) => s.toUpperCase).some)
val upper1 = optionOfStringToUpperCase("string".some)
upper1 === "STRING".some
optionOfStringToUpperCase(none[String]) === none

val toUpper: String => String = _.toUpperCase
val upper2 = toUpper.some <*> "string".some
upper2 === "STRING".some

ap2 and map2 are introduced here too.

Apply[Option].map2("hello ".some, "world".some)(_ + _) === "hello world".some
Apply[Option].map2(none[String], "world".some)(_ + _) === none[String]

val composeTwoOptions: (Option[String], Option[Int]) => Option[String] = Apply[Option].ap2(((s: String, i: Int) => s + i).some)
composeTwoOptions.apply("hi".some, 1.some) === "hi1".some
composeTwoOptions.apply("hi".some, none[Int]) === none[String]

Product left/right are important tools, and they’re declared at Apply.

The allows to omit result of computations on the left/right side.

"hello".some *> "world".some === "world".some
"hello".some <* "world".some === "hello".some
none[String] *> "world".some === none[String]
none[String] <* "world".some === none[String]

mapN magic

cats.ApplyArityFunctions is responsible for bringing map3, map4, etc.

Apply[Option].map3(2.some, 2.some, 1.some)(_ + _ + _) === 5.some

cats.syntax.TupleSemigroupalSyntax brings some magic with mapN:

(1.some, 2.some).mapN { case (a, b) => a + b } === 3.some

Magic is multiplied by zero under the hood:

private[syntax] final class Tuple3SemigroupalOps[F[_], A0, A1, A2](private val t3: Tuple3[F[A0], F[A1], F[A2]]) {
  def mapN[Z](f: (A0, A1, A2) => Z)(implicit functor: Functor[F], semigroupal: Semigroupal[F]): F[Z] = Semigroupal.map3(t3._1, t3._2, t3._3)(f)

So, there are just dozens of implementations.

Apply examples


Typically Applicative is described as applicative functor where map, ap, and pure are equally important.

We already considered Apply and Functor, hence we’re interested in pure method responsible for initialization of specified container: def pure[A](a: A): F[A]

For Either it is going to be Right(a), Option has Some(a), and so on.

Even while it seems extremely natural when we work with particular implementations it is vital to have abstraction to describe such a thing.

There are good definitions for pure and product: (Applicative Typeclass)[https://typelevel.org/cats/typeclasses/applicative.html#applicative]

Applicative[Option].pure(1) === 1.some
Applicative[Vector].pure(1) === Vector(1)

You can “replicate” values inside F:

Applicative[Option].replicateA(3, 1.some) === List(1, 1, 1).some

Applicative is composable:

Applicative[List].compose[Vector].compose[Option].pure(3) === List(Vector(3.some))

Some unit-functions that could be used when you need to preserve F-context but content doesn’t matter or should be hidden:

Applicative[Option].unit === ().some

Applicative[List].whenA(true)(List(1, 2, 3)) === List((), (), ())