How to reduce Seq of functions that return Either with short circuit behavior?

Viewed 121

Take the following example (adapted from the cats Either documentation).

Three validation functions are being applied in sequence each returning an Either. If the application of any of the functions fails a Left value is returned.

object Validators {
  def isBiggerThan5(i: Int): Either[Exception, Int] =
    if (i > 5) Either.right(i)
    else Either.left(new NumberFormatException(s"${i} is smaller than 5."))

  def isSmallerThan20(i: Int): Either[Exception, Int] =
    if (i < 20) Either.right(i)
    else Either.left(new NumberFormatException(s"${i} is bigger than 20."))

  def isEven(i: Int): Either[Exception, Int] =
    if (i % 2 == 0) Either.right(i)
    else Either.left(new NumberFormatException(s"${i} is not an even number."))
}
import Validators._

def magic(i: Int): Either[Exception, Int] =
  isBiggerThan5(i).flatMap(isSmallerThan20).flatMap(isEven)

Now imagine the three validation functions are passed as arguments:

type Ops = Int => Either[Exception, Int]

def magic(ops: Seq[Ops])(s: String): Either[Exception, String] =
  ??? // How to apply ops in sequence with short circuit behavior?

println(magic(Seq(isBiggerThan5, isSmallerThan20, isEven)))

How can we evaluate the Seq[Ops] one after another with short circuit behavior? I have played around with Seq.foldLeft or Seq.reduce but could not quite figure it out.

I am not interested in error accumulation. It should simply fail on the first validator that returns a Left.

2 Answers

foldLeftM should work.

import cats.syntax.all._
def verify(i: Int, ops: Vector[Ops]): Either[Exception, Int] =
  ops.foldLeftM(i)((i, o) => o(i))

I agree the best solution is to use foldLeftM
However, just for sake of completeness if you want a more vanilla solution, this is a perfect use case for List and tail-recursion.

type Ops[A] = A => Either[Exception, A]

def magic[A](initial: A)(ops: List[Ops[A]): Either[Exception, A] = {
  @annotation.tailrec
  def loop(remaining: List[Ops[A]], current: A): Either[Exception, A] =
    remaining match {
      case step :: tail =>
        step(current) match {
          case Right(next) =>
            loop(remaining = tail, current = next)

          case left @ Left(_) =>
            left
        }

      case Nil =>
        Right(current)
    }

  loop(remaining = ops, current = initial)
}

(after looking at all this boilerplate is when you start to appreciate cats)

Related