I'm developing a method that is supposed to persist an object, if it passes a list of conditions.
If any (or many) condition fail (or any other kind of error appears), a list with the errors should be returned, if everything goes well, a saved entity should be returned.
I was thinking about something like this (it's pseudocode, of course):
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'd like to treat it like an Option[T], that if any validation fails, instead of returning None
it gives me the list of errors...
The idea is to return an array of JSON errors...
So the question would be, is this the right way to handle these kind of situation? And what would be the way to accomplish it in Scala?
--
Oops, just posted the question and discovered Either
http://www.scala-lang.org/api/current/scala/Either.html
Anyway, I'd like to know what you think about the chosen approach, and if there's any other better alternative to handle it.
Using scalaz you have Validation[E, A]
, which is like Either[E, A]
but has the property that if E
is a semigroup (meaning things that can be concatenated, like lists) than multiple validated results can be combined in a way that keeps all the errors that occured.
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 is right-biased by default:
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 the errors occured (temp sensor error and/or twitter error or none) or the successful message. You can then switch back to \/
for convenience.
Note: The difference between Either and Validation is mainly that with Validation you can accumulate errors, but cannot flatMap
to lose the accumulated errors, while with Either you can't (easily) accumulate but can flatMap
(or in a for-comprehension) and possibly lose all but the first error message.
I think this might be of interest for you. Regardless of using scalaz/Either
/\/
/Validation
, I experienced that getting started was easy but going forward needs some additional work. The problem is, how do you collect errors from multiple erring functions in a meaningful way? Sure, you can just use Throwable
or List[String]
everywhere and have an easy time, but doesn't sound too much usable or interpretable. Imagine getting a list of errors like "child age missing" :: "IO error reading file" :: "division by zero".
So my choice is to create error hierarchies (using ADT-s), just like as one would wrap checked exceptions of Java into hierarchies. 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 when using result of erring functions with different error types, I join them under a relevant 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
on the left of e: \/
since \/
is a Bifunctor. For se: scala.Either
you might have se.left.map(f)
.
This may be further improved by providing shapeless HListIso
s to be able to draw nice error trees.
Revisions
Updated: (e: \/).vnel
lifts the failure side into a NonEmptyList
so if we have a failure we have at least one error (was: or none).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With