Scala - Futures and Eithers

2019-08-19 03:10发布

问题:

this thread gave me an idea how to structure my code: Scala-way to handle conditions in for-comprehensions?

The part in question:

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

This are the lines I do not understand:

user    <- userRes.withFailure(UserNotFound).right
authRes <- userDao.authenticate(user)

The userRes.withFailure(UserNotFound).right is mapped to userDao.authenticate(user). This will create a new Either with a Future on its right, correct?

How can

val resultFuture: Future[Either[Failure, JsResult]]

be of its type. I think instead of a JsResult there should be another future. Can anyone explain this to me?

EDIT: Since cmbaxter and Arne Claassen confirmed this, the new question is: How should I write this code, so it does not look ugly, but clean and structured?

回答1:

I believe the answer you received needlessly mixed Either's into the mix when Future's are already perfectly capable of communicating failure. The main thing you were missing was a way to get from an Option to the option's value without explicitly throwing exceptions.

I would suggest that you change the Failures object to the following:

object Failures {

  sealed trait Failure extends Exception

  // 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 Futures
  implicit class opt2future[A](opt: Option[A]) {
    def withFailure(f: Failure) = opt match {
      case None => Future.failed(f)
      case Some(x) => Future.successful(x)
    }
  }
}

Now you can map a Future[Option[A]] to a Future[A] and specify the failure condition, resulting in a for comprehension like this:

def checkGood(user: User, good: Good) =
  if (checkOwnership(user, good))
    Future.successful(good)
  else
    Future.failed(NoOwnership)

val resultFuture: Future[JsResult] = for {
  userOpt <- userDao.findUser(userId)
  user <- userOpt.withFailure(UserNotFound)
  authOpt <- userDao.authenticate(user)
  auth <- authOpt.withFailure(NotAuthenticated)
  goodOpt <- goodRes.withFailure(GoodNotFound)
  checkedGood <- checkGood(user, good)
} yield renderJson(Map("success" -> true))))

Now that you have a Future[JsResult] you can map the failed scenarios to your desired output and the success scenario is just the JsResult. Hopefully you are using this in an asynchronous framework which expects you to feed it a future and has its own failed future to error response mapping (such as Play!).