【问题标题】:scala style - how to avoid having lots of nested mapscala风格 - 如何避免有很多嵌套地图
【发布时间】:2012-08-09 09:11:46
【问题描述】:

在验证多个连续条件时,我经常会得到很多嵌套的 .map 和 .getOrElse

例如:

def save() = CORSAction { request =>
  request.body.asJson.map { json =>
    json.asOpt[Feature].map { feature =>
      MaxEntitiyValidator.checkMaxEntitiesFeature(feature).map { rs =>
        feature.save.map { feature => 
          Ok(toJson(feature.update).toString)
        }.getOrElse {
          BadRequest(toJson(
            Error(status = BAD_REQUEST, message = "Error creating feature entity")
          ))
        }
      }.getOrElse {
        BadRequest(toJson(
          Error(status = BAD_REQUEST, message = "You have already reached the limit of feature.")
        )) 
      }
    }.getOrElse {
      BadRequest(toJson(
        Error(status = BAD_REQUEST, message = "Invalid feature entity")
      )) 
    }
  }.getOrElse {
    BadRequest(toJson(
      Error(status = BAD_REQUEST, message = "Expecting JSON data")
    )) 
  }
}

你懂的

我只是想知道是否有一些惯用的方法可以让它更清晰

【问题讨论】:

    标签: scala styles


    【解决方案1】:

    这是使用 monad 可以清理代码的经典示例。例如,您可以使用 Lift 的 Box,它与 Lift 没有任何关联。然后您的代码将如下所示:

    requestBox.flatMap(asJSON).flatMap(asFeature).flatMap(doSomethingWithFeature)
    

    其中asJson 是从请求到Box[JSON] 的函数,asFeature 是从Feature 到其他Box 的函数。该框可以包含一个值,在这种情况下 flatMap 会使用该值调用函数,或者它可以是Failure 的一个实例,在这种情况下flatMap 不会调用传递给它的函数。

    如果您发布了一些可编译的示例代码,我本可以发布可编译的答案。

    【讨论】:

    • 非常感谢您的回答,我发布的代码可以编译,但我想这个例子有点太复杂了......
    • 如何在不同级别维护不同的BadRequest 响应?这似乎是阻碍直截了当的方法的关键问题。我想知道模式匹配是否可以做到这一点?
    • 没错。在命令式语言中,一旦发现错误,我就会退出该方法。但是当我尝试这样做时,我遇到了返回语句stackoverflow.com/questions/11929485/…的几个问题
    • @KimStebel 在非电梯项目中使用电梯箱的更简单方法是什么?有最小的 jar 可用吗?
    • @Richard Sitze:每个函数 asJSON、asFeature 和 doSomethingWithFeature 都可以在失败时返回 Failure,这是 Box 的子类型,包含错误消息和可选的异常。
    【解决方案2】:

    如果您不必为 None 情况返回不同的消息,这将是 理解 的理想用例。在您的情况下,您可能希望使用 Validation monad,因为您可以在 Scalaz 中找到它。示例(http://scalaz.github.com/scalaz/scalaz-2.9.0-1-6.0/doc.sxr/scalaz/Validation.scala.html)。

    在函数式编程中,你不应该抛出异常,而是让可能失败的函数返回一个 Either[A,B],按照约定,A 是失败时的结果类型,B 是结果的类型在成功的情况下。然后您可以匹配 Left(a) 或 Right(b) 来分别处理这两种情况。

    您可以将 Validation monad 视为扩展的 Either[A,B],其中将后续函数应用于 Validation 将产生结果,或者执行链中的第一个失败。

    sealed trait Validation[+E, +A] {
      import Scalaz._
    
      def map[B](f: A => B): Validation[E, B] = this match {
        case Success(a) => Success(f(a))
        case Failure(e) => Failure(e)
      }
    
      def foreach[U](f: A => U): Unit = this match {
        case Success(a) => f(a)
        case Failure(e) =>
      }
    
      def flatMap[EE >: E, B](f: A => Validation[EE, B]): Validation[EE, B] = this match {
        case Success(a) => f(a)
        case Failure(e) => Failure(e)
      }
    
      def either : Either[E, A] = this match {
        case Success(a) => Right(a)
        case Failure(e) => Left(e)
      }
    
      def isSuccess : Boolean = this match {
        case Success(_) => true
        case Failure(_) => false
      }
    
      def isFailure : Boolean = !isSuccess
    
      def toOption : Option[A] = this match {
        case Success(a) => Some(a)
        case Failure(_) => None
      }
    
    
    }
    
    final case class Success[E, A](a: A) extends Validation[E, A]
    final case class Failure[E, A](e: E) extends Validation[E, A]
    

    您的代码现在可以通过使用 Validation monad 重构为三个验证层。您基本上应该用如下验证替换您的地图:

    def jsonValidation(request:Request):Validation[BadRequest,String] = request.asJson match {
       case None => Failure(BadRequest(toJson(
          Error(status = BAD_REQUEST, message = "Expecting JSON data")
        )
       case Some(data) => Success(data)
    }
    
    def featureValidation(validatedJson:Validation[BadRequest,String]): Validation[BadRequest,Feature] = {
    validatedJson.flatMap {
      json=> json.asOpt[Feature] match {
        case Some(feature)=> Success(feature)
        case None => Failure( BadRequest(toJson(
          Error(status = BAD_REQUEST, message = "Invalid feature entity")
            )))
      }
    }
    

    }

    然后你像下面这样链接它们featureValidation(jsonValidation(request))

    【讨论】:

      【解决方案3】:

      我尝试了这个,看看模式匹配是否能以某种方式使提交的代码示例(在风格上,如果不是字面上)适应更连贯的东西。

      object MyClass {
      
        case class Result(val datum: String)
        case class Ok(val _datum: String) extends Result(_datum)
        case class BadRequest(_datum: String) extends Result(_datum)
      
        case class A {}
        case class B(val a: Option[A])
        case class C(val b: Option[B])
        case class D(val c: Option[C])
      
        def matcher(op: Option[D]) = {
          (op,
           op.getOrElse(D(None)).c,
           op.getOrElse(D(None)).c.getOrElse(C(None)).b,
           op.getOrElse(D(None)).c.getOrElse(C(None)).b.getOrElse(B(None)).a
          ) match {
            case (Some(d), Some(c), Some(b), Some(a)) => Ok("Woo Hoo!")
            case (Some(d), Some(c), Some(b), None)    => BadRequest("Missing A")
            case (Some(d), Some(c), None,    None)    => BadRequest("Missing B")
            case (Some(d), None,    None,    None)    => BadRequest("Missing C")
            case (None,    None,    None,    None)    => BadRequest("Missing D")
            case _                                    => BadRequest("Egads")
          }
        }
      }
      

      显然有一些方法可以更优化地编写它;这留给读者作为练习。

      【讨论】:

        【解决方案4】:

        我同意 Edmondo 关于使用理解的建议,但不同意关于使用验证库的部分(至少不再考虑到 2012 年以来添加到 scala 标准库的新功能)。根据我使用 scala 的经验,在使用标准库时努力想出好的声明的开发人员最终也会在使用 cat 或 scalaz 之类的库时做同样的事情,甚至更糟。也许不在同一个地方,但理想情况下,我们会解决问题,而不仅仅是移动它。

        这里是你重写的代码,它是 scala 标准库的一部分:

        def save() = CORSAction { request =>
        
          // Helper to generate the error 
          def badRequest(message: String) = Error(status = BAD_REQUEST, message)
        
          //Actual validation
          val updateEither = for {
            json    <- request.body.asJson.toRight(badRequest("Expecting JSON data"))
            feature <- json.asOpt[Feature].toRight(badRequest("Invalid feature entity"))
            rs <- MaxEntitiyValidator
                   .checkMaxEntitiesFeature(feature)
                   .toRight(badRequest("You have already reached the limit"))
          } yield toJson(feature.update).toString
        
          // Turn the either into an OK/BadRequest
          featureEither match {
            case Right(update) => Ok(update)
            case Left(error)   => BadRequest(toJson(error))
          }
        }
        

        说明

        错误处理

        我不确定您对此了解多少,但它们在行为上与 Edmondo 提供的 Validation 或 scala 库中的 Try 对象非常相似。这些对象之间的主要区别在于它们的能力和错误行为,但除此之外,它们都可以以相同的方式进行映射和平面映射。

        您还可以看到,我使用toRight 立即将选项转换为 Either,而不是在最后执行。我看到 java 开发人员有反射性地在物理上尽可能地抛出异常,但他们这样做主要是因为 try catch 机制很笨拙:如果成功,要从 try 块中获取数据,您要么需要返回它们或将它们放入块中初始化为 null 的变量中。但 scala 的情况并非如此:您可以映射 try 或 any,因此通常情况下,如果您在将结果识别为错误表示后立即将其转换为错误表示,您将获得更清晰的代码。

        为了理解

        我也知道,发现 scala 的开发人员常常对理解力感到困惑。就像在大多数其他语言中一样,这很容易理解,因为它仅用于集合的迭代,而 scala 似乎可用于许多不相关的类型。在 scala 中,调用 flatMap 函数实际上是更好的方法。编译器可能会决定使用 map 或 foreach 对其进行优化,但它仍然正确假设您在使用 for 时将获得 flatMap 行为。 在集合上调用 flatMap 的行为就像在其他语言中的 for each 一样,因此 scala for 可以像处理集合时的标准一样使用。但是您也可以在任何其他类型的对象上使用它,这些对象提供了具有正确签名的 flatMap 实现。如果你的 OK/BadRequest 也实现了 flatMap,你可以直接在 for 理解中使用 in 而不是使用中间的 Either 表示。

        因为人们对在看起来不像集合的任何东西上使用 for 感到不自在,以下是如果明确使用 flatMap 而不是 for 时函数的外观:

        def save() = CORSAction { request =>
          def badRequest(message: String) = Error(status = BAD_REQUEST, message)
          
          val updateEither = request.body.asJson.toRight(badRequest("Expecting JSON data"))
            .flatMap { json =>
              json
                .asOpt[Feature]
                .toRight(badRequest("Invalid feature entity"))
            }
            .flatMap { feature =>
               MaxEntitiyValidator
                 .checkMaxEntitiesFeature(feature)
                 .map(_ => feature)
                 .toRight(badRequest("You have already reached the limit"))
             }
             .map { rs =>
               toJson(feature.update).toString
             }
        
          featureEither match {
            case Right(update) => Ok(update)
            case Left(error)   => BadRequest(toJson(error))
          }
        }
        

        请注意,就参数范围而言,如果函数是嵌套的,而不是链式的,则 for 行为是实时的。

        结论

        我认为,除了不使用正确的框架或正确的语言功能之外,您提供的代码的主要问题是如何处理错误。一般来说,你不应该写错误路径,因为你认为你在方法结束时会堆积起来。如果您可以在错误发生时立即处理它们,那么您就可以转移到其他地方。相反,你把它们推回去的次数越多,你就会有越多的代码具有不可分割的嵌套。它们实际上是 scala 期望您在某个时候处理的所有未决错误案例的具体化。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-10-05
          • 2013-01-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多