简体   繁体   中英

How to reduce nesting on chain futures

Most of the time my Future[T] operations are dependent to previous future in the chain. I am using flatMap function with pattern matching most of the time. Such as;

findUser(userId).flatMap {
  case None => Future.successful(NotFound("No user with given id"))
  case Some(user) => findAddress(user.addressId).flatMap {
    case None => Future.successful(NotFound("No address with given id"))
    case Some(address) => findCity(address.cityId).flatMap {
      case None => Future.successful(NotFound("No city with given id"))
      case Some => Future.successful(Ok)
    }
  }
}

with this way i am able to return an object related with the problem, all branchings are being handled. But downside of this approach in my opinion (and my code reading pleasure) it is getting nested a lot. Also if the lines are too long it is impossible to track which case statement is which even with a proper formatting. So that goes to the right-bottom side of the editor.

The other way one would suggest might using for comprehension. Below is kind of a equivalent of the code above. But the difference is for-comp one is throwing an exception if the if-guard is not satisfied. Also it returns an option to use which wherever i want to use i need to call get method (which i don't want to do);

val items = for {
  user <- findUser(userId) if user.isDefined
  address <- findAddress(user.addressId) if address.isDefined
  city <- findCity(address.cityId) if address.isDefined
} yield (user.get, address.get, city.get)

Again one may suggest catching the exception but as i read from many sources catching exceptions are considered not good. Also the exception wouldn't provide which case statement didn't satisfy the condition.

Same thing applies for return statements as well. As myself coming from java and .net based languages, i am inclined to use the style below.

val user = Await.result(findUser(userId), timeout)
if (user.isEmpty) {
  return Future.successful(NotFound("No user with given id"))
}

val address = Await.result(findAddress(user.get.addressId), timeout)
if (address.isEmpty) {
  return Future.successful(NotFound("No address with given id"))
}

val city = Await.result(findUser(address.get.cityId), timeout)
if(city.isEmpty) {
  return Future.successful(NotFound("No city with given id"))
}

Future.successful(Ok)

which is definitely out of question in my understanding. First of all it makes the code-block blocking, secondly again it forces me to use get values and uses return blocks which are similar with the throwing exceptions in the matter of cutting the execution short.

Haven't been able to find an elegant solution to this. I am currently going with the nested approach which makes it harder to read

Thanks

You should use .failed futures rather than successful to communicate exceptional conditions:

sealed trait NotFoundErr
class NoUser extends Exception("No user with given id") with NotFoundErr
class NoAddress extends Exception("No address with given id") with NotFoundErr
class NoCity extends Exception("No city with given id") with NotFoundErr

def getOrElse[T](ifNot: Exception)(what: => Future[Option[T]]) = what
  .map(_.getOrElse(throw ifNot))

val items = for {
  user <- getOrElse(new NoUser)(findUser(userId))
  address <- getOrElse(new NoAddress)(findAddress(user.addressId))
  city <- getOrElse(new NoCity)(findCity(address.cityId))     
} yield (user, address, city)

items
 .map(_ => Ok)
 .recover { case e: Exception with NotFoundErr => NotFound(e.getMessage) }

You can make it look even fancier with an implicit:

object RichFuture {
   implicit class Pimped[T](val f: Future[Option[T]]) extends AnyVal {
      def orElse(what: => T) = f.map(_.getOrElse(what))
   }
}

Now, you can write the for-comprehension like:

for {
    user <- findUser(userId) orElse throw(new NoUser)
    address <- findAddress(user.addressId) orElse throw(new NoAddress)
    city <- findCity(address.cityId) orElse throw(new NoCity)
} yield (user, address, city)

The elegant solution to this problem is to use an appropriate data type to wrap the different failure cases.

I'd suggest you look into

Cats Validated or Scalaz Validation

Those types collects the operation outcome and compose well in comprehensions and possibly with futures

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM