[英]scala: how to handle validations in a functional way
我正在开发一种方法,如果它传递一个条件列表,它应该持久化一个对象。
如果任何(或许多)条件失败(或出现任何其他类型的错误),则应返回包含错误的列表,如果一切顺利,则应返回已保存的实体。
我在想这样的事情(当然是伪代码):
request.body.asJson.map { json =>
json.asOpt[Wine].map { wine =>
wine.save.map { wine =>
Ok(toJson(wine.update).toString)
}.getOrElse { errors => BadRequest(toJson(errors))}
}.getOrElse { BadRequest(toJson(Error("Invalid Wine entity")))}
}.getOrElse { BadRequest(toJson(Error("Expecting JSON data")))}
也就是说,我想把它当作Option [T],如果任何验证失败,而不是返回None
它会给我错误列表......
想法是返回一个JSON错误数组......
所以问题是,这是处理这种情况的正确方法吗? 在Scala中实现它的方法是什么?
-
哎呀,刚刚发布了问题并发现了
http://www.scala-lang.org/api/current/scala/Either.html
无论如何,我想知道你对所选方法的看法,以及是否还有其他更好的选择来处理它。
使用scalaz你有Validation[E, A]
,它类似于Either[E, A]
但具有如下属性:如果E
是半群(意思是可以连接的东西,比如列表),那么多个验证结果可以组合在一起保持发生的所有错误的方法。
例如,使用Scala 2.10-M6和Scalaz 7.0.0-M2,其中Scalaz有一个自定义的Either[L, R]
名为\\/[L, R]
的默认情况下右偏置:
import scalaz._, Scalaz._
implicit class EitherPimp[E, A](val e: E \/ A) extends AnyVal {
def vnel: ValidationNEL[E, A] = e.validation.toValidationNEL
}
def parseInt(userInput: String): Throwable \/ Int = ???
def fetchTemperature: Throwable \/ Int = ???
def fetchTweets(count: Int): Throwable \/ List[String] = ???
val res = (fetchTemperature.vnel |@| fetchTweets(5).vnel) { case (temp, tweets) =>
s"In $temp degrees people tweet ${tweets.size}"
}
这里的result
是Validation[NonEmptyList[Throwable], String]
,包含发生的所有错误(临时传感器错误和/或推特错误
或无
)或成功消息。 然后,您可以切换回\\/
以方便使用。
注意:Either和Validation之间的区别主要在于使用Validation可以累积错误,但是不能使用flatMap
来丢失累积的错误,而使用或者你不能(容易)累积但可以flatMap
(或者for-comprehension)和可能会丢失除第一条错误消息以外
我想这可能对你有用。 无论使用scalaz / Either
/ \\/
/ Validation
,我都意识到入门很容易但是前进需要一些额外的工作。 问题是,如何以有意义的方式从多个错误的函数中收集错误? 当然,你可以在任何地方使用Throwable
或List[String]
并且有一个简单的时间,但听起来不太可用或不可解释。 想象一下,得到一个错误列表,如“儿童年龄缺失”::“IO错误读取文件”::“除以零”。
所以我的选择是创建错误层次结构(使用ADT-s),就像将Java的已检查异常包装到层次结构中一样。 例如:
object errors {
object gamestart {
sealed trait Error
case class ResourceError(e: errors.resource.Error) extends Error
case class WordSourceError(e: errors.wordsource.Error) extends Error
}
object resource {
case class Error(e: GdxRuntimeException)
}
object wordsource {
case class Error(e: /*Ugly*/ Any)
}
}
然后,当使用具有不同错误类型的错误函数的结果时,我在相关的父错误类型下加入它们。
for {
wordSource <-
errors.gamestart.WordSourceError <-:
errors.wordsource.Error <-:
wordSourceCreator.doCreateWordSource(mtRandom).catchLeft.unsafePerformIO.toEither
resources <-
errors.gamestart.ResourceError <-:
GameViewResources(layout)
} yield ...
这里f <-: e
映射e: \\/
左边的函数f
,因为\\/
是一个Bifunctor。 对于se: scala.Either
你可能有se.left.map(f)
。
这可以通过提供无形 HListIso
来进一步改进,以便能够绘制漂亮的错误树。
修订
更新: (e: \\/).vnel
将故障侧提升到NonEmptyList
因此如果我们发生故障,我们至少会有一个错误(是:或者没有)。
如果您有Option
值,并且想要将它们转换为成功/失败值,则Either
使用toLeft
或toRight
方法将Option
转换为Either
。
通常, Right
表示成功,因此使用o.toRight("error message")
将Some(value)
转换为Right(value)
而将None
转换为Left("error message")
。
不幸的是,Scala默认不会识别这种右偏,所以你必须跳过一个箍(通过调用.right
方法),以便在for-comprehension中整齐地.right
你的Either
。
def requestBodyAsJson: Option[String] = Some("""{"foo":"bar"}""")
def jsonToWine(json: String): Option[Wine] = sys.error("TODO")
val wineOrError: Either[String, Wine] = for {
body <- requestBodyAsJson.toRight("Expecting JSON Data").right
wine <- jsonToWine(body).toRight("Invalid Wine entity").right
} yield wine
如果您需要一个空值,而不是使用Either[A,Option[B]]
您可以使用提升Box
,它可以有三个值:
Full
(有效结果) Empty
(没有结果,但也没有错误) Failure
(发生错误) 由于丰富的API, Box
比Either
更灵活。 当然,虽然它们是为Lift创建的,但您可以在任何其他框架中使用它们。
好吧,这是我尝试使用Either
def save() = CORSAction { request =>
request.body.asJson.map { json =>
json.asOpt[Wine].map { wine =>
wine.save.fold(
errors => JsonBadRequest(errors),
wine => Ok(toJson(wine).toString)
)
}.getOrElse (JsonBadRequest("Invalid Wine entity"))
}.getOrElse (JsonBadRequest("Expecting JSON data"))
}
wine.save如下所示:
def save(wine: Wine): Either[List[Error],Wine] = {
val errors = validate(wine)
if (errors.length > 0) {
Left(errors)
} else {
DB.withConnection { implicit connection =>
val newId = SQL("""
insert into wine (
name, year, grapes, country, region, description, picture
) values (
{name}, {year}, {grapes}, {country}, {region}, {description}, {picture}
)"""
).on(
'name -> wine.name, 'year -> wine.year, 'grapes -> wine.grapes,
'country -> wine.country, 'region -> wine.region, 'description -> wine.description,
'picture -> wine.picture
).executeInsert()
val newWine = for {
id <- newId;
wine <- findById(id)
} yield wine
newWine.map { wine =>
Right(wine)
}.getOrElse {
Left(List(ValidationError("Could not create wine")))
}
}
}
}
验证检查几个前提条件。 我仍然需要添加一个try / catch来捕获任何db错误
我仍然在寻找一种方法来改善整个事情,对我的口味感觉很多...
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.