将Iterable[Either[A,B]]缩减为Either[A, Iterable[B]]

8

我需要将一个Iterable[Either[Throwable, String]]缩减为一个Either[Throwable, Iterable[String]]。我不知道这个操作是否非常常见,也没有在Iterable特征上找到任何内容。因此,我编写了这个函数:

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})
  }

有没有人能帮我找到更好的方法,如果这个方法不行的话?

你想要实现的转换有点模糊不清。你的输入列表包含了一半是 Right[String],另一半是各种异构的 Left[Exception]。你想将它们缩减为一个异常或字符串列表。如果输入中有十个不同的异常,应该选择哪一个异常呢? - Tomasz Nurkiewicz
你说得对。我只想考虑第一个异常(或任何左值),它会隐藏其他异常,但对于我的使用情况来说是可以接受的。 - Filippo De Luca
1
这是https://dev59.com/f2w05IYBdhLWcg3weBhF的副本。 - ziggystar
3个回答

11

这个操作通常被称为序列化,并且在一些函数式语言的标准库中可用(例如Haskell)。在Scala中,您可以自己实现,也可以使用外部库,比如Scalaz。例如,假设我们有以下代码:

val xs: List[Either[String, Int]] = List(Right(1), Right(2))
val ys: List[Either[String, Int]] = List(Right(1), Left("1st!"), Left("2nd!"))
现在我们可以使用 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!)

按照要求进行。


值得一提的是,这个操作只需要外部容器是可遍历的,并且内部容器是一个应用函子。Scalaz还提供了一个名为ValidationNEL的类,它很像Either,也符合这些要求,但在ValidationNEL列表上使用sequence会收集多个错误而不是停在第一个:

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

现在我们得到:

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

您还可以在 OptionPromise 等列表上使用 sequence


2
实际上,它与Akka框架中的Future.sequence非常相似,不是吗? - Filippo De Luca
@FilippoDeLuca:没错,就是比Scalaz的更具体。 - Travis Brown
我还不能完全理解scalaz,但我必须确实尝试一下,或者更好的是,它必须尝试着来适应我 :) - Filippo De Luca
1
@FilippoDeLuca:我自己肯定不完全理解Scalaz!其中的一件好事是你不需要这样做-只需选择一些起点并四处漫步。 - Travis Brown

4
如果您不喜欢使用显式返回并想要缩短代码的同时消除模式匹配,这里提供了另一种版本:
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))

2

我总觉得return语句有点别扭,但以下代码可以实现:

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
  })

1
case Left(x) => return Left(x) 可以简写为 case l@Left(_) => return l - Kim Stebel
2
@KimStebel 是的,一开始我也是这么想的,但是结果的 EitherB 类型参数是错误的(它需要 Iterable[B] 但实际上是 B),所以 Left 是不同的 Left - 0__
1
谢谢,但我更喜欢不使用return语句。 - Filippo De Luca
1
我认为这是使用return适当的好例子(不应该对return的厌恶变成教条)。迭代处理某些异常情况时,可以提前返回。这就像一个收集collectFirst(尽管名称并不真正收集),其确实在其实现中使用了return。 - The Archetypal Paul

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接