Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala-way to handle conditions in for-comprehensions?

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.

like image 712
Andrey Chaschev Avatar asked Jun 06 '14 12:06

Andrey Chaschev


People also ask

What can you do with Scala for comprehension?

But, the Scala for comprehension can do much more! As an example and similar to the previous tutorial on using IF And Else Expressions, the for construct in Scala can return a value! 1. A simple for loop from 1 to 5 inclusive

How do you use for comprehension in Python?

Each for comprehension begins with a generator. for comprehensions will be multiple generators. For comprehension definitions have below syntax: For example n = b.name the variable n is bound to the value b.name. That statement has a similar result as writing this code outside of a for comprehension. val n = b.name

What is a for-loop in Scala?

In imperative programming languages, we use loops such as for-loop and while-loop to iterate over collections. The Scala programming language introduced a new kind of loop: the for-comprehension. Like many other Scala constructs, the for-comprehension comes directly from Haskell.

What is a comprehension?

A comprehension evaluates the body e for every binding generated by the enumerators and returns a sequence of those values. These definitions lead us to the for comprehension ideas of generators, filters, and definitions.


1 Answers

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 _ =>
        }
    }
}
like image 58
DCKing Avatar answered Oct 12 '22 23:10

DCKing