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



我有一个管理Item s的应用程序。当客户端通过某个info查询项目时,该应用程序首先尝试在数据库中查找包含该信息的现有项目。如果没有,该应用程序将

  1. 检查info是否有效。这是一个昂贵的操作(比数据库查找要贵得多(,因此应用程序只有在数据库中没有现有项目时才会执行此操作。

  2. 如果info有效,则使用info在数据库中插入一个新的Item

还有两个类,ItemDaoItemService:

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

ItemService.findByInfo(info: Info)方法非常难看。我已经尝试清理它一段时间了,但这很困难,因为涉及到三种类型(Future[Boolean]Future[Item]Future[Option[Item]](。我尝试过使用scalazOptionT来清理它,但非可选的Future也让它变得不太容易。

关于更优雅的实现有什么想法吗?

展开我的评论。

既然你已经表示愿意走monad transformers的路线,这应该是你想要的。不幸的是,由于Scala的不太出色的类型检查,这里有相当多的行噪声,但希望你能发现它足够优雅。

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
  }
}

关于代码的两条评论:

  • 我们在这里使用OptionT monad转换器来捕获Future[Option[_]]的东西,以及任何只存在于Future[_]内部的东西,我们是liftM,直到我们的OptionT[Future, _] monad
  • CCD_ 21是由CCD_。简言之,顾名思义,MonadPlus捕捉到了一种直觉,即有时单子有一种直观的组合方式(例如List(1, 2, 3) <+> List(4, 5, 6) = List(1, 2, 3, 4, 5, 6)(。在这里,我们使用它来在findByInfo返回Some(item)时短路,而不是在None上短路的通常行为(这大致类似于List(item) <+> List() = List(item)(

另一个小问题是,如果你真的想走monad transformers的路线,通常情况下,你最终会在monad transfer中构建所有东西(例如,ItemDao.findByInfo会返回一个OptionT[Future, Item](,这样你就不会有无关的OptionT.apply调用,然后在最后生成.run所有东西。

您不需要scalaz。只需将flatMap分解为两个步骤:首先,查找并验证,然后在必要时插入。类似这样的东西:

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

看起来不太糟,是吗?如果你不介意将验证与检索并行运行(资源成本略高,但平均速度可能更快(,你可以进一步简化它:

ItemDao
  .findByInfo(info)
  .zip(isValidInfo(info))
  .flatMap {
    case (None, true) => ItemDao.insertIfNotExists(info).map(Some(_))
    case (x, _) => x
  }

此外,如果项目确实存在,insertIfNotExists会返回什么?如果它返回了现有的项目,事情可能会更简单:

 isValidInfo(info)
   .filter(identity)
   .flatMap { _ => ItemDao.insertIfNotExists(info) }
   .map { item => Some(item) }  
   .recover { case _: NoSuchElementException => None }

如果您对依赖路径的类型和更高级的kinded类型感到满意,那么以下内容可能是一个优雅的解决方案:

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

这基本上是对包装器类型(例如Option[_](和内部类型进行抽象。对于没有包装类型的类型,我们使用Const[_]类型,它基本上是一个标识类型。

在scala中,许多类似的问题都可以使用代数数据类型及其高级类型系统(即路径相关类型和高级类型(来优雅地解决。请注意,现在我们有了用于执行数据库请求的单入口点exec(request: Request),而不是类似DAO的东西。

最新更新