Стиль 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")
))
}
}
Вы получаете идею
Я просто хотел узнать, есть ли какой-нибудь идиоматический способ, чтобы держать его более ясным.
Ответы
Ответ 1
Если вам не пришлось возвращать другое сообщение для случая "Нет", это было бы идеальным вариантом использования для для понимания. В вашем случае вы, вероятно, захотите использовать монадию проверки, как та, которую вы можете найти в Scalaz. Пример (http://scalaz.github.com/scalaz/scalaz-2.9.0-1-6.0/doc.sxr/scalaz/Validation.scala.html).
В функциональном программировании вы не должны генерировать исключения, но пусть функции, которые могут сбой, возвращают Либо [A, B], где по соглашению A - это тип результата в случае сбоя, а B - тип результата в случае успеха. Затем вы можете совместить с Left (a) или Right (b), чтобы обрабатывать два случая.
Вы можете думать о монаде проверки как расширенном Либо [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]
Теперь ваш код может быть реорганизован с помощью монады проверки в три уровня проверки. Вы должны в основном заменить свою карту на валидацию следующим образом:
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))
Ответ 2
Это классический пример того, где использование монады может очистить ваш код. Например, вы можете использовать Lift Box
, который никак не привязан к Lift
. Тогда ваш код будет выглядеть примерно так:
requestBox.flatMap(asJSON).flatMap(asFeature).flatMap(doSomethingWithFeature)
где asJson
- это функция от запроса к Box[JSON]
и asFeature
- это функция от Feature
до некоторого другого Box
. Ящик может содержать либо значение, и в этом случае flatMap вызывает функцию с этим значением, или может быть экземпляром Failure
, и в этом случае flatMap
не вызывает переданную ему функцию.
Если вы опубликовали код кода, который компилируется, я мог бы опубликовать ответ, который компилируется.
Ответ 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")
}
}
}
Ясно, что есть способы написать это более оптимально; это остается как упражнение для читателя.