Monads as applicative functors

(If you’re not into functional programming, this article might not make much sense)

Applicative functor (or just “applicative” in short) is a more general abstraction than a monad. In other words, monad is an applicative with extra functionality, and therefore a more specialised and stronger abstraction.

Applicative can be expressed in the following three ways:

unit + apply
unit + map2
unit + map + product   

By using an applicative functor instead of the non-applicative one, we can map by using multi-parameter curried functions (either by applying curried parameters one by one with apply, or by lifting everything into a product and mapping that).

Monad can be expressed in the following three ways:

unit + map + flatten
unit + flatmap
unit + compose

Since monad is an extension of the applicative functor, it must be possible to implement any applicative operations using any monadic operations. We can demonstrate that by implementing applicative’s apply + product using monad’s map + flatten (we won’t be needing unit).

Let’s say we have the following definition of a monad in Scala (we agreed to omit unit):

trait Monad[F[_]] {
  def map[A, B](fa: F[A], f: A => B): F[B] = ???
  def flatten[A](fa: F[F[A]]): F[A] = ???
}

Applicative’s methods can then be implemented as:

trait Monad2[F[_]] extends Monad[F] {

  def apply[A, B](fa: F[A], fab: F[A => B]): F[B] = 
    flatten(map(fab, (f: A => B) => map(fa, f)))

  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] = 
    flatten(map(fa, (a: A) => map(fb, (b: B) => (a,b))))
}

Here’s an example of using apply with a curried function, using List for simplicity.

def apply[A, B](list: List[A], f: List[A => B]): List[B] = f.flatMap(f => list.map(f))

val curriedF = (a: Int) => (b: Int) => (c: Int) => a + b + c

val result1 = apply(List(1), List(curriedF))  
val result2 = apply(List(2), result1)  
apply(List(3), result2) // List(6)

The usage is the same as in applicative case. However, there’s a significant difference - if apply is implemented using flatmap, we are taking advantage of monad’s power to chain things into series. Applicative functor’s apply and product lack this ability.

Let’s show this difference in practice. Since we were working with apply in the previous example, let’s take product now. We can provide two different implementations: one using monadic functions, and another one in standard applicative fashion (we can use standard Scala’s zip):

// Future stuff
import scala.concurrent.{Await, Future}  
import scala.concurrent.duration._  
import scala.concurrent.ExecutionContext.Implicits.global

// Monad  
def serialProduct[A, B](f1: => Future[A], f2: => Future[B]): Future[(A, B)] = 
  f1.flatMap(a => f2.map(b => (a, b)))

// Applicative  
def parallelProduct[A, B](f1: => Future[A], f2: => Future[B]): Future[(A, B)] = 
  f1 zip f2

Note that both are taking their parameters by-name instead of by-value in order to prevent futures from executing as soon as they are passed.

Let’s create the futures:

def first = Future { println("first"); Thread.sleep(2000); 1 }
def second = Future { println("second"); Thread.sleep(2000); 2 }

Using serial product, we get:

val result1 = serialProduct(first, second).map { case (a, b) => a + b }
Await.result(result1, 10 seconds)
// prints "first", then two seconds later "second"

Using parallel product, we get:

val result2 = parallelProduct(first, second).map { case (a, b) => a + b }
Await.result(result2, 10 seconds)
// performs in parallel; order of printing is indeterministic

Monad as a data structure could have methods apply and product exposed in its API, but it usually doesn’t. If you need those operations on some type T, your code should restrict T to the Applicative type class, following the “rule of least power”. However, it is common to end up working with monads in conjunction with applicatives, in which case it can be tedious to juggle between the two. In that case, some libraries also provide the “bridge” type class (in Cats it’s intuitively called Parallel) which abstracts over Monad type class by providing it with parallel applicative functionality.

Written on November 2, 2022