Home > Net >  How do you run a computation, that may fail, over a list of elements so that it terminates as soon a
How do you run a computation, that may fail, over a list of elements so that it terminates as soon a

Time:08-24

My computation that can fail is halve() below. It returns Left(errorMessage) to indicate failure and Right(value) to indicate the successful halving of a number.

def halve(n: Int): Either[String, Int] =
  if (n % 2 == 0) {
    Right(n / 2)
  } else {
    Left("cannot halve odd number")
  }

I'd like to apply the halve function to a list of Ints such that as soon as the first call to halve fails (e.g. when called with an odd number), the halveAll function immediately stops iterating over the numbers in ns and returns Left(errorMessage).

Here is one way to achieve this:

def halveAll(ns: List[Int]): Either[String, List[Int]] =
  try {
    Right(
      for {
        n <- ns
        Right(halved) = halve(n)
      } yield n
    )
  } catch {
    case ex: MatchError =>
      Left("cannot match an odd number")
  }

I would prefer an approach that does not use exceptions. Is there an idiomatic way of achieving this? I'd prefer the approach to use only functionality in the Scala 2.x standard library. If Cats or scalaz has an elegant solution, I'd be interested in hearing about it though.

Thank you!

Example usage of the halveAll function:

val allEven = List(2, 4, 6, 8)
val evenAndOdd = List(2, 4, 6, 7, 8)

println(halveAll(allEven))
println(halveAll(evenAndOdd))

CodePudding user response:

This has been asked a dozen times but I am too lazy to search for a duplicate.

Have you ever heard the FP meme "the answer is always traverse"? Well, you are now part of that, since that is exactly the function you want.

Thus, if you have cats in scope then you only need to do this:

import cats.syntax.all._

def halveAll(ns: List[Int]): Either[String, List[Int]] =
  ns.traverse(halve)

If you don't have it already in scope, and don't want to add it just for a single function then you may use the foldLeft from Gaël J answer, or implement the recursion if you really want to stop iterating, like this:

def traverse[A, E, B](list: List[A])(f: A => Either[E, B]): Either[E, List[B]] = {
  @annotation.tailrec
  def loop(remaining: List[A], acc: List[B]): Either[E, List[B]] =
    remaining match {
      case a :: tail =>
        f(a) match {
          case Right(b) =>
            loop(remaining = tail, b :: acc)

          case Left(e) =>
            Left(e)
        }

      case Nil =>
        Right(acc.reverse)
    }

  loop(remaining = list, acc = List.empty)
}

Disclaimer: What follows is only my opinion.

I have heard the argument about not including cats for a single function many times, people simply don't realize is not just one function but probably many of them in the rest of the codebase; which ultimately means you are probably re-implementing many bits of the library in a worse way and with less testing.

CodePudding user response:

The typical Scala approach (without libs) for this would be using foldLeft or a variant like this:

def halveAll(ns: List[Int]): Either[String, List[Int]] = {
  ns.foldLeft(Right(List.empty[Int])) { (acc, n) =>
    for { // for-comprehension on Either
      accVal <- acc
      x <- halve(n)
    } yield accVal :  x
  }
}

As soon as a Left is produced by halve, it will continue iterating but will not call halve on the remaining items.


If you really need to not iterate anymore, you can use a recursive approach instead.

I guess it depends the size of the list but iterating over it should not be that costly most of the time.

  • Related