The following task is quite common: do something if all given predicates are satisfied and if some predicate fails, make it obvious which failed.

Consider the contrived scenario where we are asked to save a number to the database if it satisfy all our conditions:

def isGood(x: Int): Boolean = isConditionA(x) && isConditionB(x) ...

def usage(x: Int): Unit = if (isGood(x)) saveNumber(x) else doNothing()


Some time later, we notice that some numbers are being rejected, and we have no idea why, so we decide to add logging when a number is rejected. How should we go about it?

For the sake of brevity, let us assume our condition for a number to be good is that it is even but not 42. But you can imagine that in the real world you probably have a moderate number of predicates to test.

## Stick in a println before each predicate returns

def printIfFalse(b: Boolean, log: String): Boolean = {
if (!b) { println(log) }; b
}

def isNotMagicNumber(x: Int): Boolean =
printIfFalse(x != 42, "Unacceptable 42")

def isEven(x: Int): Boolean =
printIfFalse(x % 2 == 0, "Odd number")

val check: Int => Boolean = (x: Int) => isNotMagicNumber(x) && isEven(x)

def usage(x: Int): Unit = if (check(x)) saveNumber(x) else doNothing()


This is quick but feels dirty because we eagerly printed the failure reason. A more subtle argument from the purist would be that f() && g() and g() && f() should be the same and it is not in this case because we rely on the short circuit semantics of &&.

## Using Either[String, Unit]

The issue we had was that we have no control over when the failure message is printed. What if we store the value to be printed (an idea similar to the writer monad)?

We can use the Either type, which has the fail fast semantics we want. Note that Either is only composable after 2.12.

  type Rejection = Either[String, Unit]
val isNotMagicNumber: Int => Rejection = x =>
if (x != 42) Right(()) else Left("Rejected because it is 42")
val isEven: Int => Rejection = x =>
if (x % 2 == 0) Right(()) else Left("Rejected because it is odd")
val check: Int => Either[String, Unit] = (x: Int) =>
for {
_ <- isNotMagicNumber(x)
_ <- isEven(x)
} yield ()
def usage(x: Int): Unit = {
check(x).fold(reason => println(s"Failed: \$reason"),
_ => saveNumber(x))
}


Now when we call check, we either get a failure reason, or Unit which indicates that all is good. We can choose whether to print it out or do something else with it. This looks nice.

The only awkward looking piece is the type Unit, it seems superfluous and is only needed to satisfy the requirements of Either.

However, we could make it slightly more ergonomic to use by defining a constructor:

def myCondition(check: Boolean, reason: String): Rejection


What else can we do?

## What if we define our own monad

Perhaps we can try to avoid the unnecessary Unit type and define our own succinct type. Using a library like scalaz or cats, we could define a monad instance for our type which will allow us to use in in for comprehension.

First we define our ADT:

  // +T is used because we don't want to type Fail
sealed abstract class MyCondition[+T] {
// Useful method to avoid having to do matching
def fold[R](ifSuccess: () => R, ifFailure: String => R): R = this match {
// We throw away the result
case Success(_)     => ifSuccess()
case Failed(reason) => ifFailure(reason)
}
}

// A convenient constructor
def myCondition(success: Boolean, reason: String): MyCondition[Boolean] =
if (success) Success(success)
else Failed(reason)

// We need Success to be polymorphic because the Monad typeclass requires a
// point method to lift a value into this class.
case class Success[T](t: T) extends MyCondition[T] {}
case class Failed(reason: String) extends MyCondition[Nothing] {}


Then we implement a monad instance using the library:

  implicit val pMonad: Monad[MyCondition] = new Monad[MyCondition] {
override def point[A](a: => A): MyCondition[A] = Success[A](a)
override def bind[A, B](fa: MyCondition[A])(
f: A => MyCondition[B]): MyCondition[B] = fa match {
case Success(t) => f(t)
case Failed(r)  => Failed(r)
}
}


This will allow us to use it in a for comprehension:

val check = (x: Int) => for {
_ <- myCondition(x != 42, "Is 42")
_ <- myCondition(x % 2 == 0, "Is odd")
} yield ()
def usage(x: Int) = check(x)
.fold(() => saveNumber(x), println)


That seems like a lot of code for very little, and it still seems a little awkward that Success still has an unnecessary type parameter and the boolean value it wraps is effectively unused. It is needed to implement the monad type class from the scalaz, which is more generic than what we need.

Can we trim this code down?

## Implement to the syntactic sugar

Our previous attempt was rather verbose, but what do we really need to be able to use our custom type in a for comprehension? We can have the IDE tell us what the desugared syntax looks like and implement those instead.

  sealed trait MyCondition {
def flatMap[R](f: Any => MyCondition): MyCondition = this match {
case Success   => f(()) // The desugared form suggests we don't care about input
case Failed(x) => Failed(x)
}
def map[R](f: Any => R): MyCondition = this match {
case Success   => Success // Again, we ignore input
case Failed(x) => Failed(x)
}
}

case object Success extends MyCondition
case class Failed(reason: String) extends MyCondition

def myCondition(success: Boolean, reason: String): MyCondition =
if (success) Success
else Failed(reason)

val check: Int => MyCondition = (x: Int) =>
for {
_ <- myCondition(x != 42, "Is 42")
_ <- myCondition(x % 2 == 0, "Is odd")
} yield ()

def main(args: Array[String]): Unit = {
println(check(43))
}


This is marginally nicer, but it is still a lot of code.

# Other thoughts and conclusion

Conceptually, Either[String, Unit] is isomorphic to Option[String] (except with flatmap biased towards None instead of Some) and to our MyCondition that we have seen above. While scalaz allows one to define an isomorphism between two type classes, I struggled to make it ergonomic to use in this case.

It seems to me that using Either[String, Unit] to capture the failed reason is the simplest and cleanest of the approaches discussed above.