Scala -way to handle conditions for understanding?

I am trying to create a neat design with an understanding of futures business logic. Here is an example that 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 can be seen as a non-functional or non-Scala way of handling things. Is there a better way to do this?

Please note that these errors come from different sources - some of them are at the business level ("verification of ownership"), and some of them are at the controller level ("authorization"), and some are at the db level ("object not found" ). Thus, approaches when you extract them from one common type of error may not work.

+6
source share
3 answers

The central problem is that for-understanding, they can only work on one monad at a time, in which case it is a Future monad and the only way to short-circuit the sequence of future calls is that the future will fail. This works because subsequent calls in the understanding, these are just calls to map and flatmap , and the behavior of map / flatmap if Future is unsuccessful is to return this future and not execute the provided body (i.e. the called function).

What you are trying to achieve is a summary of a workflow based on certain conditions, and not do it if not in the future. This can be done by wrapping the result in another container, call it Result[A] , which gives an understanding of the type Future[Result[A]] . Result will either contain the value of the result or be the final result. The task is as follows:

  • provides subsequent function calls contained in a previous non-exhaustive Result
  • prevent subsequent function call if Result completes

map/flatmap seems to be candidates for these types of compositions, except that we will have to call them manually, since the only map/flatmap that can evaluate for-comprehension is the result that leads to Future[Result[A]] .

Result can 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, an action is indeed a potential action, since calling it or with a final result will simply support the final result. Note: Terminator is Result[Nothing] , as it will never contain values, and any Result[+A] can be Result[Nothing] .

The end 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 end result allows short-circuited calls to functions that require the value [A] when we have already met our termination condition.

A non-limiting 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 = {} } 

A non-mathematical result allows us to provide functions [A] . For good measure, I also predefined UnitResult for functions that are purely secondary, such as goodDao.removeGood .

Now let's define your good but final conditions:

 case object UserNotFound extends Terminator case object NotAuthenticated extends Terminator case object GoodNotFound extends Terminator case object NoOwnership extends Terminator 

We now have the tools to create the workflow you were looking for. Everyone needs a function to understand, which returns a Future[Result[A]] on the right side, creating Result[A] on the left side. flatmap on Result[A] allows you to call (or short-circuited) a function that requires input [A] , and we can then map it 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 for any Future for an understanding that has completion conditions, a lot of settings can be used, but at least the Result type.

+4
source

You can clear a little for understanding 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 return to Future into a failed Future when they are None . There will be many ways to do the cleanup for comp, and this is one of the possible ways to do this.

+7
source

Do not use exceptions for expected behavior.

This is not nice in Java, and it's really not nice in Scala. Please see this question for more information on why you should avoid using exceptions for a regular flow of control. Scala is very well equipped to avoid using exceptions: you can use Either s.

The trick is to identify some failures that you might encounter, and convert Option to Either , which complete 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 understanding clear and exclusive:

 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 _ => } } } 
+7
source

Source: https://habr.com/ru/post/970448/


All Articles