【问题标题】:Avoiding deeply nested Option cascades in Scala在 Scala 中避免深度嵌套的选项级联
【发布时间】:2015-05-07 05:29:52
【问题描述】:

假设我有三个数据库访问函数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,我最终得到的代码几乎和第一个示例一样嵌套。

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

【问题讨论】:

    标签: scala nested option scala-cats


    【解决方案1】:

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

    【讨论】:

    • 这几乎正是我在玩弄代码后想出的解决方案:-)。
    • 我的转换器如下所示:def right[A, B](option: Option[A])(ifNone: B): RightProjection[B, A] = option.toRight(ifNone).right
    • 您可能需要ifNone: =&gt; B,这样即使没有失败,您也不必每次都构建失败案例。
    【解决方案2】:

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

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

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

    【讨论】:

    • 我提交了一张票来重构代码以返回Either,但还没有时间处理它。我们有很多这样的案例。
    • @Ralph 也可以。我喜欢Try,因为上面写着SuccessFailure。使用Try 并不意味着您必须抛出异常,您实际上可以拥有一个不会减慢进程的非堆栈分配异常。
    • @wheaties - 让无堆栈异常四处飘荡有点危险,因为如果它们被抛出,你不知道它们来自哪里。如果可能,最好在与他们合作时至少使用贷款模式,例如getAStacklessException(params){ e =&gt; ??? /* Don't store e anywhere or use futures! */ }.
    【解决方案3】:

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

    鉴于,在您的示例中,三个 Options 是独立的,您可以将它们视为 Applicative Functor,并使用 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 会强制执行顺序评估)。更多示例和文档可以在 herehere 找到。

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

    【讨论】:

      【解决方案4】:

      我想出了这个解决方案(基于@Rex 的解决方案和他的 cmets):

      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
      

      他们执行以下操作:

      • 当函数返回Boolean 时使用ifTruetrue 是“成功”的情况(例如:isAllowed(userId))。它实际上返回Unit,因此应该在for 理解中用作_ &lt;- ifTrue(...) { error }
      • 当函数返回 Option 时使用 none,其中 None 是“成功”案例(例如:findUser(email) 用于创建具有唯一电子邮件地址的帐户)。它实际上返回Unit,因此应该在for 理解中用作_ &lt;- none(...) { error }
      • some 用于当函数返回 OptionSome() 是“成功”情况时使用(例如:findUser(userId) 用于 GET /users/userId)。它返回Some的内容:user &lt;- 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 的结果。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-12-01
        • 2011-06-21
        • 2019-02-09
        • 1970-01-01
        • 2012-09-20
        • 2023-02-09
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多