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

Ral*_*lph 8 scala nested option scala-cats

说我有三个数据库访问功能foo,bar以及baz可在每个返回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))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

可以看出,代码严重嵌套.

相反,我使用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"))
Run Code Online (Sandbox Code Playgroud)

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

这些是更好的结构方法,以避免嵌套,同时允许特定的错误消息?

Rex*_*err 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)
Run Code Online (Sandbox Code Playgroud)

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

例外的是另一种方式去,但他们慢下来一个不少,如果失败的病例屡见不鲜.如果失败真是异常,我只会使用它.

也可以使用非本地回报,但这种特殊设置有点尴尬.我认为正确的预测Either是要走的路.如果你真的喜欢这种方式,但不喜欢把它.right放在一个地方,你可以找到一个"偏右的Either"的地方,默认情况下就像正确的投影一样(例如ScalaUtils,Scalaz等).


Ral*_*lph 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
Run Code Online (Sandbox Code Playgroud)

他们执行以下操作:

  • ifTrue当函数返回 a 时使用Boolean,表示true“成功”情况(例如:isAllowed(userId))。它实际上会返回,因此应该在理解中Unit使用。_ <- ifTrue(...) { error }for
  • none当函数返回“成功”情况时使用(例如:Option用于创建具有唯一电子邮件地址的帐户)。它实际上会返回,因此应该在理解中使用。NonefindUser(email)Unit_ <- none(...) { error }for
  • some当函数返回 anOptionSome()为“成功”情况时使用(例如:findUser(userId)对于 a GET /users/userId)。Some它返回:的内容user <- 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)
}
Run Code Online (Sandbox Code Playgroud)

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