Up your game by stacking Applicatives in Scala

Monads are very useful in many different situations.

But they get a little unwieldy when we have different Monads nested inside each other.

In these cases Monad Transformers come to the rescue. They allow us to compose different Monads into one that shares the functionalities of all of them.

But sometimes we want to combine the behavior of Applicatives in the same way. This is especially useful when we have to combine independent tasks.

In this post we will see how to do this in Scala with the use of the Cats library.

But let’s first look at an example of Monad Transformers.

A Future of Either

Image we have several long running tasks that might fail and that depend on each other. In case of failure they should propagate an error message.

This could be e.g. expensive database queries or large data processing etc.

Each task returns a Future[Either[Error, Result]].

Since this type is not so nice to handle, we can stack both the Future-Monad and the Either-Monad together and get a new type that combine both behaviors:

type FutureEither[A] = EitherT[Future, String, A]

For demonstration purposes let’s try it with some dummy tasks:

def task1(input: String): FutureEither[String] = 
  EitherT.right[Future, String, String](Future {
    Thread.sleep(2000)
    println("[LOG] task 1 executed.")
   "result of task 1"
  })

def task2(input: String): FutureEither[String] = 
  EitherT.right[Future, String, String](Future {
    Thread.sleep(2000)
    println("[LOG] task 2 executed.")
    "result of task 2"  
  }) 

def task3(input: String): FutureEither[String]  = 
  EitherT.left[Future, String, String](Future {
    Thread.sleep(2000)
    "An error occurred while running task 3."
  })

def task4(input: String): FutureEither[String]  = 
  EitherT.right[Future, String, String](Future {
    Thread.sleep(2000)
    println("[LOG] task 4 executed.")
    "result of task 4"
  })

Now those tasks can be nicely composed like this:

val result = for {
  t1 <- task1("foo")
  t2 <- task2(t1)
  t3 <- task3(t2)
  t4 <- task4(t3)
} yield ()

val evalResult = Await.result(result.value, Duration.Inf).fold(
  err => s"[FAILURE] $err",
  res => "[SUCCESS] All tasks executed successfully."
)

println(evalResult)

Note that since the tasks are dependent (one needs the output of the previous one as input) these they cannot be run in parallel.

Also the for comprehension which is syntax sugar for flatMap enforces fail fast semantics. That means after task3 fails, the whole computation is aborted and subsequent tasks won’t be executed. This can not be avoided because if one tasks fails, there is no input that we can use to pipe into the next tasks.

Here is the output:

> runMain futureeithermonadstack.Main
[info] Running futureeithermonadstack.Main 
[LOG] task 1 executed.
[LOG] task 2 executed.
[FAILURE] An error occurred while running task 3.
[success] Total time: 6 s, completed Nov 10, 2016 9:11:50 PM

I left out the imports. Find the complete source code here.

Now let’s look at another example.

Again a Future of Either but…

Now imagine that we have similar tasks as above except they don’t depend on one another.

In this case it would be nice if we could run the tasks in parallel.

Also we want to accumulate error messages so that the caller can handle all the things that went wrong at once.

To achieve this, we will use Validated from Cats, a special type for error handling.

Validated is an Applicative but not a Monad. Future is a Monad but it is also an Applicative.

How can we combine the two?

Composing Applicatives

The trick is to use the compose method of Applicative from Cats.

First we must create a type alias to get a type with only one parameter by fixing the error type to Vector[String].

type Result[A] = Validated[Vector[String], A]
type FutureResult[A] = Future[Result[A]]

Now we create a new type and throw it into Scala’s “magic hat” by annotating it with implicit. (I stole the “magic hat” metaphor from Jessica Kerr from: Functional Geekery Episode 08 – Jessica Kerr)

implicit val futureEither = {
  Applicative[Future].compose[Result]
}

Again, dummy tasks:

def task1(): FutureResult[String] = Future {
  Thread.sleep(5000)
  println("[LOG] task 1 executed.")
  "result of task 1".valid
}

def task2(): FutureResult[String] = Future {
  Thread.sleep(5000)
  Vector("An error occurred while executing task 2").invalid
}

def task3(): FutureResult[String] = Future {
  Thread.sleep(5000)
  Vector("An error occurred while executing task 3").invalid
}

def task4(): FutureResult[String] = Future {
  Thread.sleep(5000)
  println("[LOG] task 4 executed.")
  "result of task 4".valid
}

Now we can combine the task calls with Scala’s kind of strange syntax for applying Applicatives and evaluate the result:

val result = (
  task1() |@|
  task2() |@|
  task3() |@|
  task4()).tupled

val evalResult = Await.result(result, Duration.Inf).fold(
  errs => s"[FAILURE] $errs",
  _ => "[SUCCESS] All tasks executed successfully."
)

println(evalResult)

Here is the output:

> runMain futureeitherapplicativestack.Main
[info] Running futureeitherapplicativestack.Main 
[LOG] task 1 executed.
[LOG] task 4 executed.
[FAILURE] Vector(An error occurred while executing task 2, An error occurred while executing task 3)
[success] Total time: 5 s, completed Nov 10, 2016 9:41:07 PM

We see two things:

  1. The Total time was 5 seconds which indicates that the tasks were run in parallel.
  2. The computation was not aborted after the failures. Instead all tasks were executed and the two failures produced error messages that were accumulated.

Conclusion

It is really quite common to have types like Futures of Options or the like.

The code can become much cleaner when we combine the behaviors by using tools like Monad Transformers or stacks of Applicatives.

Depending of the scenario (e.g. dependent vs. independent operations) we can take one or the other tool.

Up your programming game by utilizing Monad or Applicative stacks!

Source code from this post.

Resources