Scala 样式 - 如何避免有大量嵌套映射



在验证几个连续条件时,我经常会得到很多嵌套的.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 希望您在某个时候处理的所有待处理错误案例的具体化。

相关内容

  • 没有找到相关文章

最新更新