简体   繁体   中英

Scala Future[A] and Future[Option[B]] composition

I have an app that manages Item s. 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

  1. 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.

  2. 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 Future s 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 liftM ing 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 (eg 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 (eg 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 (ie 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.

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