8

I need to reduce an Iterable[Either[Throwable, String]] to an Either[Throwable, Iterable[String]]. I don't know if this operation is pretty common or not, haven't found nothing on the Iterable trait. So I have written this function:

def reduce[A, B](xs: Iterable[Either[A, B]]): Either[A, Iterable[B]] = 
  xs.collectFirst {
    case Left(x) => x
  } match {
    case Some(x) => Left(x)
    case None => Right(xs.collect{case Right(y)=> y})
  }

Can anyone help me to find a better way if this one it isn't?

Elnur Abdurrakhimov
  • 44,533
  • 10
  • 148
  • 133
Filippo De Luca
  • 704
  • 5
  • 21
  • The transformation you want to achieve is a bit ambiguous. Your input list contains for example half of `Right[String]`s and half of various and heterogeneous `Left[Exception]`s. You want to reduce it to either one exception or list of strings. Which exception should be taken if there were e.g. ten different in the input? – Tomasz Nurkiewicz Oct 28 '12 at 21:16
  • You are right. I want to consider only the first exception (or any Left value) it will hide other but it is acceptable for my use case. – Filippo De Luca Oct 28 '12 at 21:39
  • 1
    This is a duplicate of http://stackoverflow.com/questions/7230999/how-to-reduce-a-seqeithera-b-to-a-eitherseqa-seqb. – ziggystar Oct 29 '12 at 11:32

3 Answers3

11

This operation is often called sequencing, and is available in the standard libraries of some functional languages (such as Haskell). In Scala you can either implement your own, or use an external library like Scalaz. Suppose we have the following, for example:

val xs: List[Either[String, Int]] = List(Right(1), Right(2))
val ys: List[Either[String, Int]] = List(Right(1), Left("1st!"), Left("2nd!"))

Now we can write (using Scalaz 7):

scala> import scalaz._, Scalaz._
import scalaz._
import Scalaz._

scala> xs.sequenceU
res0: Either[String,List[Int]] = Right(List(1, 2))

scala> ys.sequenceU
res1: Either[String,List[Int]] = Left(1st!)

As desired.


As a side note, this operation just requires that the outside container be traversable and that the inside container be an applicative functor. Scalaz also provides a ValidationNEL class that's a lot like Either and also fits these requirements, but using sequence on a list of ValidationNELs collects multiple errors instead of stopping at the first:

val zs: List[ValidationNEL[String, Int]] =
  List(1.successNel, "1st".failNel, "2nd".failNel)

Now we get:

scala> print(zs.sequenceU)
Failure(NonEmptyList(1st, 2nd))

You could also use sequence on a list of Options, Promises, etc.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
4

If you don't like the explicit return and want to eliminate the pattern matching while shortening the code somewhat, here is another version:

def reduce[A, B](xs: Iterable[Either[A, B]]): Either[A, Iterable[B]] =
  xs collectFirst {
    case Left(x) => Left(x)
  } getOrElse Right(xs.flatMap(_.right.toOption))
Kim Stebel
  • 41,826
  • 12
  • 125
  • 142
2

I always find return statements a bit awkward, but this following works:

def reduce[A, B](xs: Iterable[Either[A, B]]): Either[A, Iterable[B]] =
  Right(xs.collect {
    case Left(x) => return Left(x)
    case Right(x) => x
  })
0__
  • 66,707
  • 21
  • 171
  • 266
  • 1
    `case Left(x) => return Left(x)` can be shortened to `case l@Left(_) => return l` – Kim Stebel Oct 28 '12 at 21:32
  • 2
    @KimStebel yes I thought that at first, but the `B` type parameter of the resulting `Either` is wrong (it requires `Iterable[B]` but is `B` instead), so the `Left` is a different `Left`. – 0__ Oct 28 '12 at 21:33
  • 1
    Thanks, but I prefer not using the return statement. – Filippo De Luca Oct 28 '12 at 22:08
  • 1
    I think this is a good example of where the use of `return` is appropriate (the dislike of `return` shouldn't become dogma). Process something iteratively but bail early with a return for some exceptional condition. It's like a collecting collectFirst (which despite the name doesn't really collect), which does indeed use return in its implementation – The Archetypal Paul Oct 29 '12 at 14:56