8

假设我有三个数据库访问函数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))
        }
    }
}

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

相反,如果我使用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"))

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

这些是一种更好的结构方式来避免嵌套,同时允许特定的错误消息吗?

4

4 回答 4

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 等)。

于 2015-03-05T19:23:22.977 回答
2

而不是使用一个Option,而是使用一个Try。这样,您就拥有了您想要混合的 Monadic 组合以及保留错误的能力。

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

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

于 2015-03-05T18:19:53.537 回答
1

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

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

给定代码:

  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 强制执行顺序评估)。可以在此处此处找到更多示例和文档。

最后,此解决方案使用 Cats Validated,但可以轻松转换为 ScalazValidation

于 2017-03-21T17:04:55.033 回答
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当函数返回 aBoolean时使用,true是“成功”的情况(例如:)isAllowed(userId)。它实际上返回Unit,所以应该_ <- ifTrue(...) { error }for理解中使用。
  • none当函数返回“成功”情况时使用(例如:Option用于创建具有唯一电子邮件地址的帐户)。它实际上返回,所以应该在理解中使用。NonefindUser(email)Unit_ <- none(...) { error }for
  • some当函数返回一个“成功”的情况时使用(例如:Option对于a )。它返回:的内容。Some()findUser(userId)GET /users/userIdSomeuser <- 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]是调用的结果。StringXprocess

于 2015-03-06T20:07:54.647 回答