14

在验证几个连续条件时,我经常会得到很多嵌套的 .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")
    )) 
  }
}

你明白了

我只是想知道是否有一些惯用的方式来保持它更清晰

4

4 回答 4

12

如果您不必为 None 案例返回不同的消息,这将是for comprehension的理想用例。在您的情况下,您可能希望使用 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))

于 2012-08-13T07:18:34.760 回答
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")
    }
  }
}

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

于 2012-08-13T07:32:46.353 回答
3

这是使用 monad 可以清理代码的经典示例。例如,您可以使用 Lift's Box,它不以Lift任何方式绑定。然后您的代码将如下所示:

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

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

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

于 2012-08-13T07:11:43.727 回答
0

我同意 Edmondo 关于使用理解的建议,但不同意关于使用验证库的部分(至少不再考虑到 2012 年以来添加到 scala 标准库的新功能)。根据我使用 scala 的经验,在使用标准库时努力想出好的声明的开发人员最终也会在使用像猫或 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 感到不自在,以下是如果明确使用 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))
  }
}

请注意,就参数范围而言,如果函数是嵌套的,而不是链接的,则行为会生效。

结论

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

于 2021-12-07T09:00:29.313 回答