【问题标题】:Scala Future[A] and Future[Option[B]] compositionScala Future[A] 和 Future[Option[B]] 组合
【发布时间】:2016-03-09 03:55:26
【问题描述】:

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

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

  2. 如果info 有效,则将新的Item 插入到带有info 的数据库中。

还有两个类,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 来清理它,但非可选的Futures 也不太容易。

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

【问题讨论】:

  • 你有没有看过 lifting 你的非 Option 期货进入 monad 转换器?
  • @badcook 你能详细点吗?
  • @badcook 我明白你的意思。我必须自己编写提升函数(这很简单)还是可以使用现有的 scalaz 函数?
  • Scalaz 应该有一个可以调用的 .liftM[OptionT] 方法(您可能需要类型注释来帮助 Scala 的类型检查器)。

标签: scala monads scalaz


【解决方案1】:

扩展我的评论。

既然您已经表示愿意沿着单子转换器的路线走下去,那么这应该可以满足您的需求。不幸的是,由于 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
  }
}

关于代码的两个cmets:

  • 我们在这里使用OptionT monad 转换器来捕获Future[Option[_]] 的东西和任何存在于Future[_] 中的东西,我们是liftMing,直到我们的OptionT[Future, _] monad。
  • &lt;+&gt;MonadPlus 提供的操作。简而言之,顾名思义,MonadPlus 捕捉到了 monad 通常具有直观组合方式的直觉(例如List(1, 2, 3) &lt;+&gt; List(4, 5, 6) = List(1, 2, 3, 4, 5, 6))。在这里,我们使用它在findByInfo 返回Some(item) 时进行短路,而不是在None 上进行短路的通常行为(这大致类似于List(item) &lt;+&gt; List() = List(item))。

其他小提示,如果你真的想走 monad 转换器路线,通常你最终会在你的 monad 转换器中构建所有东西(例如ItemDao.findByInfo 会返回一个OptionT[Future, Item]),这样你就没有多余的东西了OptionT.apply 调用,然后.run 结束。

【讨论】:

  • 在这种情况下&lt;+&gt; 的工作方式与orElse 相同吗?
  • 是的,这是一种思考方式。实际上,与MonadPlus 等效的应用函子通常被暗示性地命名为Alternative,正是出于这个原因。编辑:哎呀,你在谈论 Scala 的实际 orElse 方法。是的,都是一样的。
【解决方案2】:

你不需要 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 }

【讨论】:

  • 我认为所有答案都是非常优雅的解决方案,但您的答案是最轻量级的。谢谢!
【解决方案3】:

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

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

【讨论】:

  • 谢谢!我明白你的意思,但我仍然看不出这会如何使编写请求更容易。在ItemService.findByInfo的当前实现中,案例类如何避免map/flatMap混乱?
  • 啊,我一定是看错了你的问题。除了一些语法糖,我没有更好的答案。我个人认为加入 OptionT 的噪音太大了。但是 YMMV ;)
猜你喜欢
  • 2020-07-16
  • 1970-01-01
  • 2016-11-08
  • 1970-01-01
  • 1970-01-01
  • 2018-06-29
  • 1970-01-01
  • 2021-04-20
  • 2016-10-27
相关资源
最近更新 更多