Scala Future[A] 和 Future[Option[B]] 组合

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

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

  1. 检查info是否有效。这是一项代价高昂的操作(比数据库查找更昂贵),因此应用程序仅在数据库中不存在现有项目时执行此操作。

  2. 如果 info 有效,则使用 info.

  3. 将新的 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 也让它变得不太容易。

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

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

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 这样的东西。

扩展我的评论。

既然你已经表示愿意沿着 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 transformer 来捕获 Future[Option[_]] 东西和任何只存在于 Future[_] 中的东西,我们正在 liftM 准备 OptionT[Future, _]单子。
  • <+>MonadPlus提供的操作。简而言之,顾名思义,MonadPlus 抓住了直觉,即 monad 通常有一种直观的组合方式(例如 List(1, 2, 3) <+> List(4, 5, 6) = List(1, 2, 3, 4, 5, 6))。在这里,我们使用它在 findByInfo returns Some(item) 时短路,而不是在 None 上短路的通常行为(这大致类似于 List(item) <+> List() = List(item)).

其他小提示,如果你真的想沿着 monad transformers 路线走下去,通常你最终会在你的 monad transformer 中构建所有东西(例如 ItemDao.findByInfo 会 return 一个 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 return 是什么意思?如果它 returned 现有项目,事情可能会更简单:

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