Scala: how to handle validations efficiently

I am developing a method that should save an object if it passes a list of conditions.

If any condition (or a lot) is not fulfilled (or some other kind of error appears), the list with errors should be returned; if everything is ok, the saved object should be returned.

I was thinking of something similar (this, of course, is pseudocode):

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")))} 

That is, I would like to consider it as Option [T], that if any check fails, instead of returning None it will give me a list of errors ...

The idea is to return an array of JSON errors ...

So the question is, is it right to deal with this situation? And what would it be like to do in Scala?

-

Oops, just posted a question and found that

http://www.scala-lang.org/api/current/scala/Either.html

In any case, I would like to know what you think about the chosen approach, and if there is another better alternative for handling it.

+7
scala error-handling
Aug 22 '12 at 6:17
source share
4 answers

Using scalaz, you have Validation[E, A] , which is similar to Either[E, A] , but has the property that if E is a semigroup (which means elements that can be combined, such as lists), than a few confirmed The results can be combined in a way that contains all the errors that have occurred.

Using Scala 2.10-M6 and Scalaz 7.0.0-M2, for example, where Scalaz has a custom Either[L, R] named \/[L, R] , which defaults to the right side:

 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}" } 

Here, result is a Validation[NonEmptyList[Throwable], String] , either containing all errors (temperature sensor error and / or twitter error , either )) or a successful message. You can return to \/ for convenience.

Note. The difference between Either and Validation lies mainly in the fact that during verification you can accumulate errors, but you ca flatMap lose flatMap errors, while using it you cannot (easily) accumulate flatMap , but you can flatMap (or in understanding) and, possibly , lose everything except the first error message.

About error hierarchies

I think this may interest you. Regardless of using scalaz / Either / \/ / Validation , I experienced that getting started was easy, but more work is needed in the future. The problem is, how do you collect errors from several erroneous functions in a meaningful way? Of course, you can just use Throwable or List[String] everywhere and have an easy time, but not too useful or interpretable. Imagine you get a list of errors, such as "child age missing" :: "I / O error reading file" :: "division by zero".

Thus, my choice is to create error hierarchies (using ADTs), just as you could wrap checked Java exceptions in a hierarchy. For example:

 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) } } 

Then, using the result of erroneous functions with different types of errors, I join them under the corresponding parent error type.

 for { wordSource <- errors.gamestart.WordSourceError <-: errors.wordsource.Error <-: wordSourceCreator.doCreateWordSource(mtRandom).catchLeft.unsafePerformIO.toEither resources <- errors.gamestart.ResourceError <-: GameViewResources(layout) } yield ... 

Here f <-: e maps the function f left of e: \/ , since \/ is a bifunctor. For se: scala.Either you can have se.left.map(f) .

This can be further improved by providing a shapeless HListIso to be able to draw beautiful error trees.

Revisions

Updated: (e: \/).vnel raises the failure side in NonEmptyList , so if we have a failure, we have at least one error (was: or not).

+13
Aug 22 2018-12-12T00:
source share

If you have Option values ​​and want to turn them into success / failure values, you can turn Option into Either using the toLeft or toRight .

Normally a Right represents success, so use o.toRight("error message") to turn Some(value) into Right(value) and None into Left("error message") .

Unfortunately, Scala does not recognize this right shift by default, so you need to go through the hoop (by calling the .right method) to carefully compose your Either for understanding.

 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 
+4
Aug 22 '12 at 9:15
source share

If you need an empty value, instead of using Either[A,Option[B]] you can use lift Box , which can have three values:

  • Full (there is a valid result)
  • Empty (no result, but no errors)
  • Failure (an error occurred)

Box more flexible than Either due to its rich API . Of course, although they were created for Lift, you can use them in any other framework.

+2
Aug 22 2018-12-12T00
source share

well this is my attemp using 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")) } 

And wine.save looks like this:

 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"))) } } } } 

Verifies the verification of several prerequisites. I still need to add try / catch to catch any db error

I'm still looking for a way to improve it all, I really like the wordy ...

0
Aug 23 '12 at 6:45
source share



All Articles