Partially functional

You can't wait for inspiration. You have to go after it with a club.

Conditional flatMap - tweaking sequences of effects with boolean conditions

• scala

I found myself in a situation when several Futures had to execute in a predefined sequence and one of those Futures was to be executed only under a certain condition. I immediately reached for the cats-provided flatMap syntax (cats.syntax.flatMap._) and my pet ifM:

  _ <- condition.pure.ifM(service.call(params), ().pure)

However, the above doesn’t look good. It’s hard to see what’s going on. So I tinkered with cats and made the more pleasing to the eye:

  _ <- service.call(params) onlyIf condition

After all,

In the original language design great care was taken to ensure that the syntax would allow programmers to create natural looking DSLs.

www.scala-lang.org

Original solution

import scala.concurrent.ExecutionContext.Implicits.global

import cats.instances.future._
import cats.syntax.applicative._
import cats.syntax.flatMap._

for {
  vatEligibility <- viewModel[VatServiceEligibility]
  _ <- s4l.saveForm(vatEligibility.setAnswer(question, data.answer))
  exit = data.answer == question.exitAnswer
  _ <- exit.pure.ifM(keystore.cache(INELIGIBILITY_REASON, question.name), ().pure)
} yield ...

Let’s focus on this line:

exit.pure.ifM(keystore.cache(INELIGIBILITY_REASON, question.name), ().pure)

This lifts the boolean condition exit into the Future context using .pure syntax. pure method is defined on the Applicative type class and if we import cats.syntax.applicative._ we can lift any value into an effect F, provided there is an implicit instance of Applicative[F] available in scope. (Read Eugene’s article to understand how implicits are resolved)

We can make further use of the cats library to enrich any Future[Boolean] (indeed, a boolean in any monadic context) with the ifM method. ifM is introduced by the import of cats.syntax.flatMap._ and allows for flatMapping different expressions, depending on what’s in the box (i.e. the boolean value in the context, on which we added the ifM extension method).

Just like flatMap takes a function A => F[B], ifM takes two functions of this shape, but only flatMaps one of them. The first parameter to ifM is called ifTrue and the second one is ifFalse and which one gets flatmapped is obvious. I made the conditional service call in the ifTrue part, leaving the ifFalse as a successfully completed Future of Unit.

But when I slept on it and re-visited the line the next day, I thought I would much rather have it written like this:

  _ <- keystore.cache(INELIGIBILITY_REASON, question.name) onlyIf exit

Natural looking DSL

Remember the part about natural looking DSLs? With the help of an implicit class and some cat-herding skills, I was able to make my dream come true:

implicit class CustomApplicativeOps[F[_], A](fa: => F[A])(implicit F: Applicative[F]) {
  def onlyIf(condition: Boolean): F[Unit] =
    if (condition) F.map(fa)(_ => ()) else F.pure(())
}

Final solution

With this implicit class imported into scope, the final for-comprehension looks like this:

import scala.concurrent.ExecutionContext.Implicits.global

import cats.instances.future._
import cats.syntax.applicative._
import cats.syntax.flatMap._

for {
  vatEligibility <- viewModel[VatServiceEligibility]
  _ <- s4l.saveForm(vatEligibility.setAnswer(question, data.answer))
  exit = data.answer == question.exitAnswer
  _ <- keystore.cache(INELIGIBILITY_REASON, question.name) onlyIf exit
} yield ...

And that’s all, folks.

PS: cats will provide whenA syntax from version 1.0. Check it out when it comes out. It can be used to achieve similar sort of thing.