You’ve probably heard of Monads or the phrase “monadic operations” (flatMap
). You may also have heard of “Cartesians” or
“Applicatives”. What are these patterns and why do you need them?
You can find complete examples of the concepts discussed on my blog in my Github repo . In particular, check out the applicatives and futures and applicatives and validation examples
What is an Applicative?
Cats describes it thus:
Applicative encodes working with multiple independent effects
What exactly does this mean? Well in simple terms it means we can perform a number of operations, perhaps in parallel, that don’t know or care about each other. Some examples are:
- Calling some external systems in parallel - For example given a user id we want to fetch her profile, order history and message inbox from three different systems. The only input to the three systems is the user id, there is no dependency between the three calls.
- Validating a form - First name and last name should be non-empty but there is no relationship between the two. We don’t need to know a user’s first name to validate her last name.
In essence an Applicative lets us perform N operations independently, then it aggregates the results for us. Lets look at some code:
import cats.Applicative
import cats.instances.future._
import scala.concurrent.duration._
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
object ApplicativeFutures {
def fetchFirstName: Future[String] = { println("fetching first name"); Future.successful("John") }
def fetchLastName: Future[String] = { println("fetching last name"); Future.successful("Doe") }
def main(args: Array[String]): Unit = {
val eventualFullName = Applicative[Future].map2(fetchFirstName, fetchLastName) (_ + " " + _)
val fullName = Await.result(eventualFullName, 1.second)
println(fullName)
}
}
As both Futures complete successfully the console output is:
fetch first name
fetch last name
John Doe
Let’s now fail the first Future and check the console:
def fetchFirstName: Future[String] = Future.failed(new Exception("BOOM!"))
fetching first name
fetching last name
Exception in thread "main" java.lang.Exception: BOOM!
eventualFullName
fails as we expect but notice how we still see the two fetch calls even though one failed. This is the independence in action
How does an Applicative differ from a Monad?
We can rewrite the previous example in the more familiar monadic/for-comprehension style:
def fetchFirstName: Future[String] = { println("fetching first name"); Future.successful("John") }
def fetchLastName: Future[String] = { println("fetching last name"); Future.successful("Doe") }
def main(args: Array[String]): Unit = {
val eventualFullName = for {
firstName <- fetchFirstName
lastName <- fetchLastName
} yield firstName + " " + lastName
val fullName = Await.result(eventualFullName, 1.second)
println(fullName)
}
Output:
fetching first name
fetching last name
John Doe
Let’s fail the first future as before:
def fetchFirstName: Future[String] = Future.failed(new Exception("BOOM!"))
Output:
fetching first name
Exception in thread "main" java.lang.Exception: BOOM!
Notice how the second call doesn’t happen. This is becuase monadic operations are dependent on each other. In this example we are actually calling fetchFirstName
, waiting for a successfull response then calling fetchLastName
. If the first call fails we don’t make it to the second. This has some serious implications
- Monadic operations operate sequentially not concurrently. That’s great when we have a dependency between the operations e.g. lookup user_id based on email then fetch the inbox based on the user_id. But for independent operations monadic calls are very inefficient as they are inherently sequential. More experienced developers may realise that the previous example can be optimised by moving the fetch calls outside the for comprehension then flat mapping the resulting futures. This is a hack required because Monads are the wrong tool for the job
- Monads fail fast which makes them poor for form validation and similar use cases. Once something “fails” the operation aborts
Why would you use an Applicative?
If you’ve read this far you should be able to answer this question yourself. In simple terms we use Applicatives when we want to perform N independent operations in parallel and aggregate the results. We’ve already looked at an example of calling multiple services in parallel so let’s take a quick look at an example for performing form style validation:
import cats.Applicative
import cats.data.Validated
import cats.data.Validated.{Invalid, Valid}
import cats.instances.list._
import cats.syntax.validated._
object ApplicativeValidation {
type ValidationError = List[String]
type ErrorsOr[A] = Validated[ValidationError, A]
def validate(key: String, value: String): ErrorsOr[String] = {
if (value.nonEmpty) value.valid else List(s"$key is empty").invalid
}
def main(args: Array[String]): Unit = {
val errorOrName = Applicative[ErrorsOr].map2(validate("first name", "john"), validate("last name", "doe"))(_ + " " + _)
errorOrName match {
case Invalid(errors) => println("errors: " + errors.mkString(", "))
case Valid(fullName) => println(fullName)
}
}
}
The data structure is slightly different due to the use of Validated. However the structure of the code is basically the same as the previous example.
Cartesian aka Semigroupal
At the start of this post I mentioned that Applicatives are specialised forms of Cartesians (now known as “Semigroupal” in Cats speak). An example will illustrate it better than I can describe it:
def main(args: Array[String]): Unit = {
val eventuallyBothNames: Future[(String, String)] = Semigroupal[Future].product(fetchFirstName, fetchLastName)
val eventualFullName = eventuallyBothNames.map { case (firstName, lastName) => firstName + " " + lastName }
val fullName = Await.result(eventualFullName, 1.second)
println(fullName)
}
Cartesian’s product()
function accepts two parameters and returns a tuple of the parameters wrapped in the given context e.g. Future. There’s nothing wrong with Cartesians/Semigroupals, it’s just that Applicative functors are typically more useful. This is especially true as Applicative includes map2, map3, mapN functions unlike Cartesian’s simple product(x, y) function
Where to go from here
Applicatives are actually quite a generic concept, in future posts I’ll focus on some specialised forms of applicative, notably:
ValidatedNel - Ideal for validating forms, config files etc Traversable - Simplifies operating on a collection of elemements in parallel