在验证几个连续条件时,我经常会得到很多嵌套的.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")
))
}
}
你明白了
我只是想知道是否有一些惯用的方法可以使其更清晰
如果您不必为 None 情况返回不同的消息,这将是理解的理想用例。在你的情况下,你可能想使用验证monad,就像你可以在Scalaz中找到的那个。示例 ( http://scalaz.github.com/scalaz/scalaz-2.9.0-1-6.0/doc.sxr/scalaz/Validation.scala.html )。
在函数式编程中,不应抛出异常,而应让可能失败的函数返回 Both[A,B],按照惯例,A 是失败时的结果类型,B 是成功情况下的结果类型。然后,您可以与 Left(a) 或 Right(b) 进行匹配,以灵活地处理这两种情况。
您可以将验证 monad 视为扩展的 Both[A,B],其中将后续函数应用于验证将产生结果或执行链中的第一次失败。
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]
现在,可以使用验证 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))
这是一个典型的示例,说明使用 monad 可以清理代码。例如,您可以使用 Lift 的 Box
,它与Lift
没有任何联系。然后你的代码将如下所示:
requestBox.flatMap(asJSON).flatMap(asFeature).flatMap(doSomethingWithFeature)
其中asJson
是从请求到Box[JSON]
的函数,asFeature
是从Feature
到其他Box
的函数。该框可以包含一个值,在这种情况下,flatMap 调用具有该值的函数,或者它可以是 Failure
的实例,在这种情况下,flatMap
不会调用传递给它的函数。
如果你发布了一些编译的示例代码,我本可以发布一个编译的答案。
我尝试这样做,看看模式匹配是否提供了某种方式来使提交的代码示例(风格,如果不是字面意思)适应更连贯的内容。
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")
}
}
}
显然,有一些方法可以更优化地写出来;这是留给读者的练习。
我同意 Edmondo 关于用于理解的建议,但不同意关于使用验证库的部分(至少考虑到自 2012 年以来添加到 scala 标准库的新功能,现在不再如此)。根据我对 scala 的经验,那些努力用标准库想出好声明的开发人员最终也会在使用 cats 或 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 提供的验证或 scala 库中的 Try 对象非常相似。这些对象之间的主要区别在于它们的能力和行为有误差,但除此之外,它们都可以以相同的方式进行映射和平面映射。
您还可以看到我使用 toRight 立即将选项转换为 Both,而不是在最后执行此操作。我看到 java 开发人员有反射性地在物理上尽可能抛出异常,但他们这样做主要是因为 try catch 机制很笨拙:如果成功,要从 try 块中获取数据,您要么需要返回它们,要么将它们放在初始化为 null 的变量中。但情况并非如此:您可以映射 try 或两者之一,因此一般来说,如果您在识别结果后立即将其转换为错误表示,您会得到更清晰的代码,因为它们被识别为不正确。
为了理解
我也知道,发现 scala 的开发人员经常对理解感到困惑。这与大多数其他语言一样是完全可以理解的,因为仅用于集合的迭代,而 scala 似乎可用于许多不相关的类型。在scala中,for实际上是调用函数flatMap的更好方法。编译器可能会决定使用 map 或 foreach 对其进行优化,但它仍然是正确的,假设您在使用 for 时将获得 flatMap 行为。在集合上调用 flatMap 的行为与其他语言中的 for 类似,因此 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))
}
}
注意,就参数作用域而言,对于行为 live 如果函数嵌套,则不链接。
结论
我认为,除了没有使用正确的框架或正确的语言功能之外,您提供的代码的主要问题是如何处理错误。通常,您不应该在方法末尾堆积起来之后编写错误路径。如果您可以在错误发生时立即处理错误,则可以转到其他内容。相反,你越是把它们推回去,你就越会拥有具有不可分割嵌套的代码。它们实际上是 scala 希望您在某个时候处理的所有待处理错误案例的具体化。