7

How to convert List[Either[String, Int]] to Either[List[String], List[Int]] using a method similar to cats sequence? For example, xs.sequence in the following code

import cats.implicits._
val xs: List[Either[String, Int]] = List(Left("error1"), Left("error2"))
xs.sequence

returns Left(error1) instead of required Left(List(error1, error2)).

KevinWrights' answer suggests

val lefts = xs collect {case Left(x) => x }
def rights = xs collect {case Right(x) => x}
if(lefts.isEmpty) Right(rights) else Left(lefts)

which does return Left(List(error1, error2)), however does cats provide out-of-the-box sequencing which would collect all the lefts?

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • 2
    Maybe this: `xs.traverse(e => Validated.fromEither(e).toValidatedNec).toEither`? Can not test it right now, so it may have typos. – Luis Miguel Mejía Suárez Jun 07 '19 at 20:59
  • 1
    Possible duplicate of [Scala: Combine Either per the whole List with Either per elements](https://stackoverflow.com/questions/56258434/scala-combine-either-per-the-whole-list-with-either-per-elements) – Mario Galic Jun 07 '19 at 22:22
  • 2
    @MarioGalic I've already mentioned the proposed duplicate in the (now deleted) comments few hours ago, but then decided that it's not completely the same: the `List(_)` part is still needed, and I wasn't 100% sure whether there is some convenience method that wraps elements into lists automatically. Also, I like the title of your question: it's a seriously good canonical title! So, I didn't close (also because I'm hesitant when closing something as duplicate of a question with my own answer). I upvoted it for the canonical title alone. – Andrey Tyukin Jun 07 '19 at 23:41

3 Answers3

6

Another variation on the same theme (similar to this answer), all imports included:

import scala.util.Either
import cats.data.Validated
import cats.syntax.traverse._
import cats.instances.list._

def collectErrors[A, B](xs: List[Either[A, B]]): Either[List[A], List[B]] = {
  xs.traverse(x => Validated.fromEither(x.left.map(List(_)))).toEither
}

If you additionally import cats.syntax.either._, then the toValidated becomes available, so you can also write:

xs.traverse(_.left.map(List(_)).toValidated).toEither

and if you additionally replace the left.map by bimap(..., identity), you end up with @DmytroMitin's wonderfully concise solution.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
4

This solution doesn't use cats, but from Scala 2.13, you can use of partitionMap:

    def convert[L,R](input: List[Either[L,R]]): Either[List[L], List[R]] = {
      val (left, right) = input.partitionMap(identity)
      if (left.isEmpty) Right(right) else Left(left)
    }

    println(convert(List(Left("error1"), Left("error2"))))
    // Left(List(error1, error2))
    println(convert(List(Right(1), Left("2"), Right(3), Left("4"))))
    // Left(List(2, 4))
    println(convert(List(Right(1), Right(2), Right(3), Right(4))))
    // Right(List(1, 2, 3, 4))
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
Valy Dia
  • 2,781
  • 2
  • 12
  • 32
3

Try

xs.traverse(_.toValidated.bimap(List(_), identity)).toEither

// List(Left("error1"), Left("error2")) => Left(List("error1", "error2"))
// List(Right(10), Right(20))           => Right(List(10, 20))
// List(Right(10), Left("error2"))      => Left(List("error2"))
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • it seems that we don't even need `toValidated/toEither` as `bimap` is available in `EitherOps` : `xs.traverse(_.bimap(List(_), identity))` – Bogdan Vakulenko Jun 08 '19 at 17:29
  • 2
    @BogdanVakulenko We do need `toValidated`. Otherwise behavior will be different ("`Left(error1)` instead of required `Left(List(error1, error2))`" as OP wrote). – Dmytro Mitin Jun 08 '19 at 18:01
  • 2
    I think you meant `Left(List(error1))` instead of `Left(List(error1, error2))`. The `List(_)` would still be there, but it would always contain only the first error. – Andrey Tyukin Jun 09 '19 at 11:14