I am trying to create a neat construction with for-comprehension for business logic built on futures. Here is a sample which contains a working example based on Exception handling:
(for {
// find the user by id, findUser(id) returns Future[Option[User]]
userOpt <- userDao.findUser(userId)
_ = if (!userOpt.isDefined) throw new EntityNotFoundException(classOf[User], userId)
user = userOpt.get
// authenticate it, authenticate(user) returns Future[AuthResult]
authResult <- userDao.authenticate(user)
_ = if (!authResult.ok) throw new AuthFailedException(userId)
// find the good owned by the user, findGood(id) returns Future[Option[Good]]
goodOpt <- goodDao.findGood(goodId)
_ = if (!good.isDefined) throw new EntityNotFoundException(classOf[Good], goodId)
good = goodOpt.get
// check ownership for the user, checkOwnership(user, good) returns Future[Boolean]
ownership <- goodDao.checkOwnership(user, good)
if (!ownership) throw new OwnershipException(user, good)
_ <- goodDao.remove(good)
} yield {
renderJson(Map(
"success" -> true
))
})
.recover {
case ex: EntityNotFoundException =>
/// ... handle error cases ...
renderJson(Map(
"success" -> false,
"error" -> "Your blahblahblah was not found in our database"
))
case ex: AuthFailedException =>
/// ... handle error cases ...
case ex: OwnershipException =>
/// ... handle error cases ...
}
However this might be seen as a non-functional or non-Scala way to handle the things. Is there a better way to do this?
Note that these errors come from different sources - some are at the business level ('checking ownership') and some are at controller level ('authorization') and some are at db level ('entity not found'). So approaches when you derive them from a single common error type might not work.
Don't use exceptions for expected behaviour.
It's not nice in Java, and it's really not nice in Scala. Please see this question for more information about why you should avoid using exceptions for regular control flow. Scala is very well equipped to avoid using exceptions: you can use Eithers.
The trick is to define some failures you might encounter, and convert your Options into Eithers that wrap these failures.
// Failures.scala
object Failures {
sealed trait Failure
// Four types of possible failures here
case object UserNotFound extends Failure
case object NotAuthenticated extends Failure
case object GoodNotFound extends Failure
case object NoOwnership extends Failure
// Put other errors here...
// Converts options into Eithers for you
implicit class opt2either[A](opt: Option[A]) {
def withFailure(f: Failure) = opt.fold(Left(f))(a => Right(a))
}
}
Using these helpers, you can make your for comprehension readable and exception free:
import Failures._
// Helper function to make ownership checking more readable in the for comprehension
def checkGood(user: User, good: Good) = {
if(checkOwnership(user, good))
Right(good)
else
Left(NoOwnership)
}
// First create the JSON
val resultFuture: Future[Either[Failure, JsResult]] = for {
userRes <- userDao.findUser(userId)
user <- userRes.withFailure(UserNotFound).right
authRes <- userDao.authenticate(user)
auth <- authRes.withFailure(NotAuthenticated).right
goodRes <- goodDao.findGood(goodId)
good <- goodRes.withFailure(GoodNotFound).right
checkedGood <- checkGood(user, good).right
} yield renderJson(Map("success" -> true)))
// Check result and handle any failures
resultFuture.map { result =>
result match {
case Right(json) => json // serve json
case Left(failure) => failure match {
case UserNotFound => // Handle errors
case NotAuthenticated =>
case GoodNotFound =>
case NoOwnership =>
case _ =>
}
}
}
You could clean up the for comprehension a little to look like this:
for {
user <- findUser(userId)
authResult <- authUser(user)
good <- findGood(goodId)
_ <- checkOwnership(user, good)
_ <- goodDao.remove(good)
} yield {
renderJson(Map(
"success" -> true
))
}
Assuming these methods:
def findUser(id:Long) = find(id, userDao.findUser)
def findGood(id:Long) = find(id, goodDao.findGood)
def find[T:ClassTag](id:Long, f:Long => Future[Option[T]]) = {
f(id).flatMap{
case None => Future.failed(new EntityNotFoundException(implicitly[ClassTag[T]].runtimeClass, id))
case Some(entity) => Future.successful(entity)
}
}
def authUser(user:User) = {
userDao.authenticate(user).flatMap{
case result if result.ok => Future.failed(new AuthFailedException(userId))
case result => Future.successful(result)
}
}
def checkOwnership(user:User, good:Good):Future[Boolean] = {
val someCondition = true //real logic for ownership check goes here
if (someCondition) Future.successful(true)
else Future.failed(new OwnershipException(user, good))
}
The idea here is to use flatMap to turn things like Options that are returned wrapped in Futures into failed Futures when they are None. There are going to be a lot of ways to do clean up that for comp and this is one possible way to do it.
The central challenge is that for-comprehensions can only work on one monad at a time, in this case it being the Future monad and the only way to short-circuit a sequence of future calls is for the future to fail. This works because the subsequent calls in the for-comprehension are just map and flatmap calls, and the behavior of a map/flatmap on a failed Future is to return that future and not execute the provided body (i.e. the function being called).
What you are trying to achieve is the short-cicuiting of a workflow based on some conditions and not do it by failing the future. This can be done by wrapping the result in another container, let's call it Result[A], which gives the comprehension a type of Future[Result[A]]. Result would either contain a result value, or be a terminating result. The challenge is how to:
provide subsequent function calls the value contained by a prior non-terminating Result
prevent the subsequent function call from being evaluated if the Result is terminating
map/flatmap seem like the candidates for doing these types of compositions, except we will have to call them manually, since the only map/flatmap that the for-comprehension can evaluate is one that results in a Future[Result[A]].
Result could be defined as:
trait Result[+A] {
// the intermediate Result
def value: A
// convert this result into a final result based on another result
def given[B](other: Result[B]): Result[A] = other match {
case x: Terminator => x
case v => this
}
// replace the value of this result with the provided one
def apply[B](v: B): Result[B]
// replace the current result with one based on function call
def flatMap[A2 >: A, B](f: A2 => Future[Result[B]]): Future[Result[B]]
// create a new result using the value of both
def combine[B](other: Result[B]): Result[(A, B)] = other match {
case x: Terminator => x
case b => Successful((value, b.value))
}
}
For each call, the action is really a potential action, as calling it on or with a terminating result, will simply maintain the terminating result. Note that Terminator is a Result[Nothing] since it will never contain a value and any Result[+A] can be a Result[Nothing].
The terminating result is defined as:
sealed trait Terminator extends Result[Nothing] {
val value = throw new IllegalStateException()
// The terminator will always short-circuit and return itself as
// the success rather than execute the provided block, thus
// propagating the terminating result
def flatMap[A2 >: Nothing, B](f: A2 => Future[Result[B]]): Future[Result[B]] =
Future.successful(this)
// if we apply just a value to a Terminator the result is always the Terminator
def apply[B](v: B): Result[B] = this
// this apply is a convenience function for returning this terminator
// or a successful value if the input has some value
def apply[A](opt: Option[A]) = opt match {
case None => this
case Some(v) => Successful[A](v)
}
// this apply is a convenience function for returning this terminator or
// a UnitResult
def apply(bool: Boolean): Result[Unit] = if (bool) UnitResult else this
}
The terminating result makes it possible to to short-circuit calls to functions that require a value [A] when we've already met our terminating condition.
The non-terminating result is defined as:
trait SuccessfulResult[+A] extends Result[A] {
def apply[B](v: B): Result[B] = Successful(v)
def flatMap[A2 >: A, B](f: A2 => Future[Result[B]]): Future[Result[B]] = f(value)
}
case class Successful[+A](value: A) extends SuccessfulResult[A]
case object UnitResult extends SuccessfulResult[Unit] {
val value = {}
}
The non-teminating result makes it possible to provide the contained value [A] to functions. For good measure, I've also predefined a UnitResult for functions that are purely side-effecting, like goodDao.removeGood.
Now let's define your good, but terminating conditions:
case object UserNotFound extends Terminator
case object NotAuthenticated extends Terminator
case object GoodNotFound extends Terminator
case object NoOwnership extends Terminator
Now we have the tools to create the the workflow you were looking for. Each for comprehention wants a function that returns a Future[Result[A]] on the right-hand side, producing a Result[A] on the left-hand side. The flatMap on Result[A] makes it possible to call (or short-circuit) a function that requires an [A] as input and we can then map its result to a new Result:
def renderJson(data: Map[Any, Any]): JsResult = ???
def renderError(message: String): JsResult = ???
val resultFuture = for {
// apply UserNotFound to the Option to conver it into Result[User] or UserNotFound
userResult <- userDao.findUser(userId).map(UserNotFound(_))
// apply NotAuthenticated to AuthResult.ok to create a UnitResult or NotAuthenticated
authResult <- userResult.flatMap(user => userDao.authenticate(user).map(x => NotAuthenticated(x.ok)))
goodResult <- authResult.flatMap(_ => goodDao.findGood(goodId).map(GoodNotFound(_)))
// combine user and good, so we can feed it into checkOwnership
comboResult = userResult.combine(goodResult)
ownershipResult <- goodResult.flatMap { case (user, good) => goodDao.checkOwnership(user, good).map(NoOwnership(_))}
// in order to call removeGood with a good value, we take the original
// good result and potentially convert it to a Terminator based on
// ownershipResult via .given
_ <- goodResult.given(ownershipResult).flatMap(good => goodDao.removeGood(good).map(x => UnitResult))
} yield {
// ownership was the last result we cared about, so we apply the output
// to it to create a Future[Result[JsResult]] or some Terminator
ownershipResult(renderJson(Map(
"success" -> true
)))
}
// now we can map Result into its value or some other value based on the Terminator
val jsFuture = resultFuture.map {
case UserNotFound => renderError("User not found")
case NotAuthenticated => renderError("User not authenticated")
case GoodNotFound => renderError("Good not found")
case NoOwnership => renderError("No ownership")
case x => x.value
}
I know that's a whole lot of setup, but at least the Result type can be used for any Future for-comprehension that has terminating conditions.
Related
I wanted to handle some exceptions in ZIO using catchAll or catchSome as the below :
object Test extends App {
def run(args: List[String]) =
myApp.fold(_ => 1, _ => 0)
val myApp =
for {
_ <- putStrLn(unsafeRun(toINT("3")).toString)
} yield ()
def toINT(s: String): IO[IOException, Int]= {
IO.succeed(s.toInt).map(v => v).catchAll(er =>IO.fail(er))
}
the code succeeded in case I passed a valid format number but it's unable to handle the exception in case I passed invalid format and idea ??
s.toInt gets evaluated outside of the IO monad. What happens is that you evaluate s.toInt first and try to pass the result of that to IO.succeed, but an exception has already been thrown before you can pass anything to IO.succeed. The name of succeed already basically says that you are sure that whatever you pass it is a plain value that cannot fail.
The docs suggest using Task.effect, IO.effect, or ZIO.effect for lifting an effect that can fail into ZIO.
Here is a program that worked for me:
val program =
for {
int <- toINT("3xyz")
_ <- putStrLn(int.toString)
} yield ()
def toINT(s: String): Task[Int] = {
ZIO.fromTry(Try(s.toInt))
}
rt.unsafeRun(program.catchAll(t => putStrLn(t.getMessage)))
I have the following Slick code that given an id returns a customer (if exists). If there's a problem (such as connectivity lost) a Failure clause will throw an exception:
def read (id: Int): Future[Option[Customer]] = {
val db = // ....
val customers = TableQuery[CustomerDB]
val action = customers.filter(_.id === id).result
val future = db.run(action.asTry)
future.map{
case Success(s) =>
if (s.length>0)
Some(s(0))
else
None
case Failure(f) => throw new Exception (f.getMessage)
}
}
Now, my understanding is that instead of using try/catch/finally of exceptions, in Scala one should use Try. In addition, no exceptions should be thrown. But if the exception is not thrown, how to notify the upper layer that a problem occurred?
Future itself does already have Try inside. So, I would say that you need to just flatten (also you code a bit complicated, I simplified):
future.flatMap {
case Success(s) => Future.successful(s.headOption)
case Failure(f) => Future.failed(f)
}
Result Future when in failed state notifies caller that execution failed (with wrapped original exception). Otherwise, successful.
The right way to do report errors is by using Either.
trait Error
case class NotFound(id: Int) extends Error
case class QueryFailed(msg: String) extends Error
def read (id: Int): Future[Either[Error, Customer]] = {
val db = // ....
val customers = TableQuery[CustomerDB]
val action = customers.filter(_.id === id).result
val future = db.run(action.asTry)
future.map{
case Success(s) =>
if (s.length>0)
Right(s(0))
else
Left(NotFound(id))
case Failure(f) => Left(QueryFailed(f.getMessage))
}
}
Ok so, in general you can use Future.successful or Future.failed(msg: String) to "signal" the upper level (aka calling method) you got the value or not.
Better approach
A good approach is however to use .recoverWith{} on a Future in case of failure.
For example:
def getUserFromCloud (userId: String): Future[String] = Future{
cloudProviderApi.getUsername(userId)
}.recoverWith{
Future.failed(s"$userId does not exist.")
}
What about the calling method?
Well you just map the success with and underscode and deal with the error by using recover:
getUserFromCloud("test").map(_ => {
//In case of success
}).recover{
//In case of failure, like return BadRequest.
}
More on recover and recoverWith in case you are interested: Scala recover or recoverWith
I have an app that manages Items. When the client queries an item by some info, the app first tries to find an existing item in the db with the info. If there isn't one, the app would
Check if info is valid. This is an expensive operation (much more so than a db lookup), so the app only performs this when there isn't an existing item in the db.
If info is valid, insert a new Item into the db with info.
There are two more classes, ItemDao and ItemService:
object ItemDao {
def findByInfo(info: Info): Future[Option[Item]] = ...
// This DOES NOT validate info; it assumes info is valid
def insertIfNotExists(info: Info): Future[Item] = ...
}
object ItemService {
// Very expensive
def isValidInfo(info: Info): Future[Boolean] = ...
// Ugly
def findByInfo(info: Info): Future[Option[Item]] = {
ItemDao.findByInfo(info) flatMap { maybeItem =>
if (maybeItem.isDefined)
Future.successful(maybeItem)
else
isValidInfo(info) flatMap {
if (_) ItemDao.insertIfNotExists(info) map (Some(_))
else Future.successful(None)
}
}
}
}
The ItemService.findByInfo(info: Info) method is pretty ugly. I've been trying to clean it up for a while, but it's difficult since there are three types involved (Future[Boolean], Future[Item], and Future[Option[Item]]). I've tried to use scalaz's OptionT to clean it up but the non-optional Futures make it not very easy either.
Any ideas on a more elegant implementation?
To expand on my comment.
Since you've already indicated a willingness to go down the route of monad transformers, this should do what you want. There is unfortunately quite a bit of line noise due to Scala's less than stellar typechecking here, but hopefully you find it elegant enough.
import scalaz._
import Scalaz._
object ItemDao {
def findByInfo(info: Info): Future[Option[Item]] = ???
// This DOES NOT validate info; it assumes info is valid
def insertIfNotExists(info: Info): Future[Item] = ???
}
object ItemService {
// Very expensive
def isValidInfo(info: Info): Future[Boolean] = ???
def findByInfo(info: Info): Future[Option[Item]] = {
lazy val nullFuture = OptionT(Future.successful(none[Item]))
lazy val insert = ItemDao.insertIfNotExists(info).liftM[OptionT]
lazy val validation =
isValidInfo(info)
.liftM[OptionT]
.ifM(insert, nullFuture)
val maybeItem = OptionT(ItemDao.findByInfo(info))
val result = maybeItem <+> validation
result.run
}
}
Two comments about the code:
We are using the OptionT monad transformer here to capture the Future[Option[_]] stuff and anything that just lives inside Future[_] we're liftMing up to our OptionT[Future, _] monad.
<+> is an operation provided by MonadPlus. In a nutshell, as the name suggests, MonadPlus captures the intuition that often times monads have an intuitive way of being combined (e.g. List(1, 2, 3) <+> List(4, 5, 6) = List(1, 2, 3, 4, 5, 6)). Here we're using it to short-circuit when findByInfo returns Some(item) rather than the usual behavior to short-circuit on None (this is roughly analogous to List(item) <+> List() = List(item)).
Other small note, if you actually wanted to go down the monad transformers route, often times you end up building everything in your monad transformer (e.g. ItemDao.findByInfo would return an OptionT[Future, Item]) so that you don't have extraneous OptionT.apply calls and then .run everything at the end.
You don't need scalaz for this. Just break your flatMap into two steps:
first, find and validate, then insert if necessary. Something like this:
ItemDao.findByInfo(info).flatMap {
case None => isValidInfo(info).map(None -> _)
case x => Future.successful(x -> true)
}.flatMap {
case (_, true) => ItemDao.insertIfNotExists(info).map(Some(_))
case (x, _) => Future.successful(x)
}
Doesn't look too bad, does it? If you don't mind running validation in parallel with retrieval (marginally more expensive resource-vise, but likely faster on average), you could further simplify it like this:
ItemDao
.findByInfo(info)
.zip(isValidInfo(info))
.flatMap {
case (None, true) => ItemDao.insertIfNotExists(info).map(Some(_))
case (x, _) => x
}
Also, what does insertIfNotExists return if the item does exist? If it returned the existing item, things could be even simpler:
isValidInfo(info)
.filter(identity)
.flatMap { _ => ItemDao.insertIfNotExists(info) }
.map { item => Some(item) }
.recover { case _: NoSuchElementException => None }
If you are comfortable with path-dependent type and higher-kinded type, something like the following can be an elegant solution:
type Const[A] = A
sealed trait Request {
type F[_]
type A
type FA = F[A]
def query(client: Client): Future[FA]
}
case class FindByInfo(info: Info) extends Request {
type F[x] = Option[x]
type A = Item
def query(client: Client): Future[Option[Item]] = ???
}
case class CheckIfValidInfo(info: Info) extends Request {
type F[x] = Const[x]
type A = Boolean
def query(client: Client): Future[Boolean] = ???
}
class DB {
private val dbClient: Client = ???
def exec(request: Request): request.FA = request.query(dbClient)
}
What this does is basically to abstract over both the wrapper type (eg. Option[_]) as well as inner type. For types without a wrapper type, we use Const[_] type which is basically an identity type.
In scala, many problems alike this can be solved elegantly using Algebraic Data Type and its advanced type system (i.e path-dependent type & higher-kinded type). Note that now we have single point of entry exec(request: Request) for executing db requests instead of something like DAO.
Scala's Try is very useful.
I'd like to use that pattern, but log all exceptions.
How can I do this?
Define the following helper:
import scala.util.{Try, Failure}
def LogTry[A](computation: => A): Try[A] = {
Try(computation) recoverWith {
case e: Throwable =>
log(e)
Failure(e)
}
}
Then you can use it as you would use Try, but any exception will be logged through log(e).
Starting Scala 2.13, the chaining operation tap can be used to apply a side effect (in this case some logging) on any value while returning the original value:
import util.chaining._
val x = Try("aa".toInt).tap(_.failed.foreach(println))
// java.lang.NumberFormatException: For input string: "aa"
// x: Try[Int] = Failure(java.lang.NumberFormatException: For input string: "aa")
Or an equivalent pattern matching version:
val x = Try("aa".toInt).tap { case Failure(e) => println(e) case _ => }
// java.lang.NumberFormatException: For input string: "aa"
// x: Try[Int] = Failure(java.lang.NumberFormatException: For input string: "aa")
The tap chaining operation applies a side effect (in this case println or some logging) on a value (in this case a Try) while returning the original unmodified value on which tap is applied (the Try):
def tap[U](f: (A) => U): A
You can tweak it even further using implicit class
def someMethod[A](f: => A): Try[A] = Try(f)
implicit class LogTry[A](res: Try[A]) {
def log() = res match {
case Success(s) => println("Success :) " + s); res
case Failure(f) => println("Failure :( " + f); res
}
}
Now you can call someMethod and on its result call log like this:
scala> someMethod(1/0).log
Failure :( java.lang.ArithmeticException: / by zero
and
scala> someMethod(1).log
Success :) 1
Of course println method inside implicit class can be substituted with any logging you want.
You used the term "exceptions" which is ambiguous. (java.lang.)Throwable is the root of anything that can be placed behind the throw term. java.lang.Exception is one of the two descendants of Throwable (the other being java.lang.Error). Further making this ambiguous is java.lang.RuntimeException, a descendant of Exception, which is probably where you mostly want to spend your logging time (unless you are doing lower level application framework or hardware driver implementations).
Assuming you are wanting to log literally ALL instances of Throwable, then you would need something like this (NOT RECOMMENDED):
def logAtThrowable(f: => A): Try[A] =
try
Try(f) match {
case failure # Failure(throwable) =>
log(s"Failure: {throwable.getMessage}")
failure
case success # _ =>
//uncomment out the next line if you want to also log Success-es
//log(s"Success: {throwable.getMessage}")
success
}
catch throwable: Throwable => {
//!NonFatal pathway
log(s"Failure: {throwable.getMessage}")
throw throwable
}
The external try/catch is required to capture all the Throwable instances which are filtered away by scala.util.control.NonFatal within the Try's try/catch block.
That said...there is a Java/JVM rule: you should never define a catch clause at the resolution of Throwable (again, unless you are doing lower level application framework or hardware driver implementations).
Following the intention of this rule, you would need to narrow the Throwable to you only emitted logging at the finer grained level, say something more refined, like java.lang.RuntimeException. If so, the code would look like this (recommended):
def logAtRuntimeException(f: => A): Try[A] =
Try(f) match {
case failure # Failure(throwable) =>
throwable match {
case runtimeException: RuntimeException =>
log(s"Failure: {runtimeException.getMessage}")
}
failure
case success # _ =>
success
}
In both code snippets above, you will notice that I used match as opposed to .recoverWith. This is to facilitate easily adding a rethrow that works. It turns out that all the methods on Try are themselves also wrapped with try/catch blocks. This means that if you want to log the Throwable and then rethrow it, if you are using one of the Try methods like recoverWith, the rethrow is immediately recaught and placed into a Failure thereby completely undermining the value of the intentional rethrow. By using match, the rethrow is guaranteed to succeed as it remains outside any of the Try methods.
If you would like to see more of the rabbit holes around this particular area, I created a blog post of my own exploration.
Very often i end up with lots of nested .map and .getOrElse when validating several consecutives conditions
for example:
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")
))
}
}
You get the idea
I just wanted to know if there's some idiomatic way to keep it more clear
If you hadn't had to return a different message for the None case this would be an ideal use-case for for comprehension. In your case , you probably want to use the Validation monad, as the one you can find in Scalaz. Example ( http://scalaz.github.com/scalaz/scalaz-2.9.0-1-6.0/doc.sxr/scalaz/Validation.scala.html ).
In functional programming, you should not throw exceptions but let functions which can fail return an Either[A,B], where by convention A is the type of result in case of failure and B is the type of result in case of success. You can then match against Left(a) or Right(b) to handle, reespectively, the two cases.
You can think of the Validation monad as an extended Either[A,B] where applying subsequent functions to a Validation will either yield a result, or the first failure in the execution chain.
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]
Your code now can be refactored by using the Validation monad into three validation layers. You should basically replace your map with a validation like the following:
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")
)))
}
}
}
And then you chain them like the following featureValidation(jsonValidation(request))
This is a classic example of where using a monad can clean up your code. For example you could use Lift's Box, which is not tied to Lift in any way. Then your code would look something like this:
requestBox.flatMap(asJSON).flatMap(asFeature).flatMap(doSomethingWithFeature)
where asJson is a Function from a request to a Box[JSON] and asFeature is a function from a Feature to some other Box. The box can contain either a value, in which case flatMap calls the function with that value, or it can be an instance of Failure and in that case flatMap does not call the function passed to it.
If you had posted some example code that compiles, I could have posted an answer that compiles.
I tried this to see if pattern matching offered someway to adapt the submitted code sample (in style, if not literally) to something more coherent.
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")
}
}
}
Clearly there are ways to write this more optimally; this is left as an exercise for the reader.
I agree with Edmondo suggestion of using for comprehension but not with the part about using a validation library (At least not anymore given the new features added to scala standard lib since 2012). From my experience with scala, dev that struggle to come up with nice statement with the standard lib will also end up doing the same of even worst when using libs like cats or scalaz. Maybe not at the same place, but ideally we would solve the issue rather than just moving it.
Here is your code rewritten with for comprehension and either that is part of scala standard lib :
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))
}
}
Explanations
Error handling
I'm not sure how much you know about either but they are pretty similar in behaviour as Validation presented by Edmondo or Try object from the scala library. Main difference between those object regard their capability and behaviour with errors, but beside that they all can be mapped and flat mapped the same way.
You can also see that I use toRight to immediately convert the option into Either instead of doing it at the end. I see that java dev have the reflex to throw exception as far as they physically can, but they mostly do so because the try catch mechanism is unwieldy: in case of success, to get data out of a try block you either need to return them or put them in a variable initialized to null out of the block. But this is not the case is scala: you can map a try or an either, so in general, you get a more legible code if you turn results into error representation as soon as have identified it as they are identified as incorrect.
For comprehension
I also know that dev discovering scala are often quite puzzled by for comprehension. This is quite understandable as in most other language, for is only used for iteration over collections while is scala, it seem to use usable on a lot of unrelated types. In scala for is actually more nicer way to call the function flatMap. The compiler may decide to optimize it with map or foreach but it remain correct assume that you will get a flatMap behavior when you use for.
Calling flatMap on a collection will behave like the for each would in other language, so scala for may be used like a standard for when dealing with collection. But you can also use it on any other type of object that provide an implementation for flatMap with the correct signature. If your OK/BadRequest also implement the flatMap, you may be able to use in directly in the for comprehension instead of usong an intermediate Either representation.
For the people are not at ease with using for on anything that do not look like a collection, here is is how the function would look like if explicitly using flatMap instead of 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))
}
}
Note that in term of parameter scope, for behave live if the function where nested, not chained.
Conclusion
I think that more than not using the right framework or the right language feature, the main issue with the code your provided is how errors are dealt with. In general, you should not write error paths as after thought that you pile up at the end of the method. If you can deal with the error immediately as they occur, that allow you to move to something else. On the contrary, the more you push them back, the more you will have code with inextricable nesting. They are actually a materialization of all the pending error cases that scala expect you to deal with at some point.