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

时间:2015-03-05 18:09:05

标签: scala nested option scala-cats

假设我有三个数据库访问函数foobarbaz,每个函数都可以返回Option[A],其中A是某个模型类,并且电话相互依赖。

我想按顺序调用函数,并且在每种情况下,如果找不到值,则返回相应的错误消息(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,我的代码几乎与第一个示例一样嵌套。

这是一种更好的方法来构建它以避免嵌套,同时允许特定的错误消息吗?

4 个答案:

答案 0 :(得分:7)

您可以使用正确的投影来使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)

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

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

也可以使用非本地回报,但这种特殊设置有点尴尬。我认为Either的正确预测是可行的方法。如果你真的喜欢以这种方式工作但又不喜欢将.right放在所有地方,那么有很多地方你可以找到一个“右偏的Either”,它默认就像正确的投影一样(例如ScalaUtils ,Scalaz等。)。

答案 1 :(得分:2)

我不会使用Option而是使用Try。这样你就可以将Monadic组合与保留错误的能力相结合。

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

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

答案 2 :(得分:1)

我知道这个问题在一段时间后得到了回答,但我想替代接受的答案。

鉴于此,在您的示例中,三个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和{{3}}。

最后,此解决方案使用Cats Validated,但很容易转换为Scalaz Validation

答案 3 :(得分:0)

我提出了这个解决方案(基于@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时使用
  • Booleantrue为“成功”情况(例如:isAllowed(userId))。它实际上会返回Unit,因此应该在_ <- ifTrue(...) { error }理解中用作for
  • 如果函数返回noneOption为“成功”的情况(例如:None用于创建具有唯一电子邮件地址的帐户),则使用
  • findUser(email)。它实际上会返回Unit,因此应该在_ <- none(...) { error }理解中用作for
  • 如果函数返回someOption为“成功”情况(例如:Some()findUser(userId)),则使用
  • GET /users/userId。它返回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)
}

这会返回Either[String, X],其中String是错误消息,X是调用process的结果。