开发者

How to reduce Seq[Either[A,B]] to Either[A,Seq[B]]?

开发者 https://www.devze.com 2023-03-31 17:40 出处:网络
Given a sequence of eithers开发者_Python百科 Seq[Either[String,A]] with Left being an error message. I want to obtain an Either[String,Seq[A]] where I get a Right (which will be a Seq[A]), if all elem

Given a sequence of eithers开发者_Python百科 Seq[Either[String,A]] with Left being an error message. I want to obtain an Either[String,Seq[A]] where I get a Right (which will be a Seq[A]), if all elements of the sequence are Right. If there is at least one Left (an error message), I'd like to obtain the first error message or a concatenation of all error messages.

Of course you can post cats or scalaz code but I'm also interested in code not using it.

Edit

I've changed the title, which originally asked for an Either[Seq[A],Seq[B]] to reflect the body of the message.


Edit: I missed that the title of your question asked for Either[Seq[A],Seq[B]], but I did read "I'd like to obtain the first error message or a concatenation of all error messages", and this would give you the former:

def sequence[A, B](s: Seq[Either[A, B]]): Either[A, Seq[B]] =
  s.foldRight(Right(Nil): Either[A, List[B]]) {
    (e, acc) => for (xs <- acc.right; x <- e.right) yield x :: xs
  }

scala> sequence(List(Right(1), Right(2), Right(3)))
res2: Either[Nothing,Seq[Int]] = Right(List(1, 2, 3))

scala> sequence(List(Right(1), Left("error"), Right(3)))
res3: Either[java.lang.String,Seq[Int]] = Left(error)

Using Scalaz:

val xs: List[Either[String, Int]] = List(Right(1), Right(2), Right(3))

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


Given a starting sequence xs, here's my take:

xs collectFirst { case x@Left(_) => x } getOrElse
  Right(xs collect {case Right(x) => x})

This being in answer to the body of the question, obtaining only the first error as an Either[String,Seq[A]]. It's obviously not a valid answer to the question in the title


To return all errors:

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

Note that rights is defined as a method, so it'll only be evaluated on demand, if necessary


It should work:

def unfoldRes[A](x: Seq[Either[String, A]]) = x partition {_.isLeft} match {
  case (Nil, r) => Right(r map {_.right.get})
  case (l, _) => Left(l map {_.left.get} mkString "\n")
}

You split your result in left and right, if left is empty, build a Right, otherwise, build a left.


Here is the scalaz code:

_.sequence


Starting in Scala 2.13, most collections are provided with a partitionMap method which partitions elements based on a function which maps items to either Right or Left.

In our case, we don't even need a function that transforms our input into Right or Left to define the partitioning since we already have Rights and Lefts. Thus a simple use of identity!

Then it's just a matter of matching the resulting partitioned tuple of lefts and rights based on whether or not there are lefts:

eithers.partitionMap(identity) match {
  case (Nil, rights)       => Right(rights)
  case (firstLeft :: _, _) => Left(firstLeft)
}

// * val eithers: List[Either[String, Int]] = List(Right(1), Right(2), Right(3))
//         => Either[String,List[Int]] = Right(List(1, 2, 3))
// * val eithers: List[Either[String, Int]] = List(Right(1), Left("error1"), Right(3), Left("error2"))
//         => Either[String,List[Int]] = Left("error1")

Details of the intermediate step (partitionMap):

List(Right(1), Left("error1"), Right(3), Left("error2")).partitionMap(identity)
// => (List[String], List[Int]) = (List("error1", "error2"), List(1, 3))


Building on Kevin's solution, and stealing a bit from Haskell's Either type, you can create a method partitionEithers like so:

def partitionEithers[A, B](es: Seq[Either[A, B]]): (Seq[A], Seq[B]) =
  es.foldRight (Seq.empty[A], Seq.empty[B]) { case (e, (as, bs)) =>
    e.fold (a => (a +: as, bs), b => (as, b +: bs))
  }

And use that to build your solution

def unroll[A, B](es: Seq[Either[A, B]]): Either[Seq[A], Seq[B]] = {
  val (as, bs) = partitionEithers(es)
  if (!as.isEmpty) Left(as) else Right(bs)
}


I'm not used to use Either - here is my approach; maybe there are more elegant solutions:

def condense [A] (sesa: Seq [Either [String, A]]): Either [String, Seq [A]] = {
  val l = sesa.find (e => e.isLeft)
  if (l == None) Right (sesa.map (e => e.right.get)) 
  else Left (l.get.left.get)
}

condense (List (Right (3), Right (4), Left ("missing"), Right (2)))
// Either[String,Seq[Int]] = Left(missing)
condense (List (Right (3), Right (4), Right (1), Right (2)))
// Either[String,Seq[Int]] = Right(List(3, 4, 1, 2))

Left (l.get.left.get) looks a bit funny, but l itself is a Either [A, B], not an Either [A, Seq[B]], and needs rewrapping.


My answer is similar to @Garrett Rowe's: But it uses foldLeft (Also see: Why foldRight and reduceRight are NOT tail recursive?) and prepends to Seq rather than appending to Seq (See: Why is appending to a list bad?).

scala> :paste
// Entering paste mode (ctrl-D to finish)

def partitionEitherSeq[A,B](eitherSeq: Seq[Either[A,B]]): (Seq[A], Seq[B]) =
  eitherSeq.foldLeft(Seq.empty[A], Seq.empty[B]) { (acc, next) =>
  val (lefts, rights) = acc
  next.fold(error => (lefts :+ error, rights), result => (lefts, rights :+ result))
}

// Exiting paste mode, now interpreting.

partitionEitherSeq: [A, B](eitherSeq: Seq[Either[A,B]])(Seq[A], Seq[B])

scala> partitionEitherSeq(Seq(Right("Result1"), Left("Error1"), Right("Result2"), Right("Result3"), Left("Error2")))
res0: (Seq[java.lang.String], Seq[java.lang.String]) = (List(Error1, Error2),List(Result1, Result2, Result3))
0

精彩评论

暂无评论...
验证码 换一张
取 消