在 Scala 中避免深度嵌套的选项级联

Avoiding deeply nested Option cascades in Scala

假设我有三个数据库访问函数 foobarbaz,每个 return Option[A] 其中 A 是某些模型 class,调用相互依赖。

我想按顺序调用这些函数,在每种情况下,如果找不到该值,return 会显示一条适当的错误消息 (None)。

我当前的代码如下所示:

Input is a URL: /x/:xID/y/:yID/z/:zID

foo(xID) match {
  case None => Left(s"$xID is not a valid id")
  case Some(x) =>
    bar(yID) match {
      case None => Left(s"$yID is not a valid id")
      case Some(y) =>
        baz(zID) match {
          case None => Left(s"$zID is not a valid id")
          case Some(z) => Right(process(x, y, z))
        }
    }
}

可以看出,代码嵌套很糟糕。

如果相反,我使用 for 理解,我无法给出具体的错误消息,因为我不知道哪一步失败了:

(for {
  x <- foo(xID)
  y <- bar(yID)
  z <- baz(zID)
} yield {
  Right(process(x, y, z))
}).getOrElse(Left("One of the IDs was invalid, but we do not know which one"))

如果我使用 mapgetOrElse,我最终得到的代码几乎与第一个示例一样嵌套。

是否有更好的结构方式来避免嵌套,同时允许特定的错误消息?

我不会使用 Option,而是使用 Try。这样你就拥有了你想要的 Monadic 组合和保留错误的能力。

def myDBAccess(..args..) =
 thingThatDoesStuff(args) match{
   case Some(x) => Success(x)
   case None => Failure(new IdError(args))
 }

我在上面假设你实际上并没有控制这些功能,也不能重构它们给你一个非Option。如果你这样做了,那么只需替换 Try.

您可以通过使用正确的投影来使 for 循环正常工作。

def ckErr[A](id: String, f: String => Option[A]) = (f(id) match {
  case None => Left(s"$id is not a valid id")
  case Some(a) => Right(a)
}).right

for {
  x <- ckErr(xID, foo)
  y <- ckErr(yID, bar)
  z <- ckErr(zID, baz)
} yield process(x,y,z)

这仍然有点笨拙,但它的优点是作为标准库的一部分。

异常是另一种方法,但如果失败案例很常见,它们会使速度减慢很多。如果失败真的很特别,我只会使用它。

也可以使用非本地 returns,但对于这个特定的设置来说有点尴尬。我认为 Either 的正确预测是可行的方法。如果你真的喜欢这样工作,但不喜欢把 .right 到处都是,你可以在很多地方找到 "right-biased Either",默认情况下它就像正确的投影(例如 ScalaUtils, Scalaz 等)。

我想到了这个解决方案(基于@Rex 的解决方案和他的评论):

def ifTrue[A](boolean: Boolean)(isFalse: => A): RightProjection[A, Unit.type] =
  Either.cond(boolean, Unit, isFalse).right

def none[A](option: Option[_])(isSome: => A): RightProjection[A, Unit.type] =
  Either.cond(option.isEmpty, Unit, isSome).right

def some[A, B](option: Option[A])(ifNone: => B): RightProjection[B, A] =
  option.toRight(ifNone).right

他们执行以下操作:

  • ifTrue 用于函数 returns a Booleantrue 是 "success" 的情况(例如:isAllowed(userId)) .它实际上 returns Unit 所以应该在 for 理解中用作 _ <- ifTrue(...) { error }
  • none 用于函数 returns 和 OptionNone 是 "success" 的情况(例如:findUser(email) 用于创建具有唯一电子邮件地址的帐户)。它实际上 returns Unit 所以应该在 for 理解中用作 _ <- none(...) { error }
  • some 用于函数 returns 和 OptionSome() 是 "success" 的情况(例如:findUser(userId) 用于GET /users/userId)。它returns的内容Someuser <- some(findUser(userId)) { s"user $userId not found" }.

它们用于 for 理解:

for {
  x <- some(foo(xID)) { s"$xID is not a valid id" }
  y <- some(bar(yID)) { s"$yID is not a valid id" }
  z <- some(baz(zID)) { s"$zID is not a valid id" }
} yield {
  process(x, y, z)
}

This returns an Either[String, X] 其中 String 是一条错误消息,X 是调用 process.[=38= 的结果]

我知道这个问题很久以前就有人回答了,但我想提供一个已接受答案的替代方案。

鉴于在您的示例中,三个 Option 是独立的,您可以将它们视为 Applicative Functors 并使用 Cats 中的 ValidatedNel 来简化和聚合不愉快路径的处理。

给定代码:

  import cats.data.Validated.{invalidNel, valid}

  def checkOption[B, T](t : Option[T])(ifNone : => B) : ValidatedNel[B, T] = t match {
    case None => invalidNel(ifNone)
    case Some(x) => valid(x)

  def processUnwrappedData(a : Int, b : String, c : Boolean) : String = ???

  val o1 : Option[Int] = ???
  val o2 : Option[String] = ???
  val o3 : Option[Boolean] = ???

然后您可以复制获得您想要的内容:

//import cats.syntax.cartesian._
( 
  checkOption(o1)(s"First option is not None") |@|
  checkOption(o2)(s"Second option is not None") |@|
  checkOption(o3)(s"Third option is not None")
 ) map (processUnwrappedData)

这种方法将允许您聚合失败,这在您的解决方案中是不可能的(因为使用 for-comprehensions 强制执行顺序评估)。可以找到更多示例和文档 here and here.

最后这个解决方案使用了 Cats Validated 但可以很容易地转换为 Scalaz Validation