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

时间:2016-03-09 03:55:26

标签: scala monads scalaz

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

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

  2. 如果info有效,请使用Item将新info插入数据库。

  3. 还有两个课程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]])。我尝试使用scalaz的{​​{1}}进行清理,但非可选的OptionT也不是很容易。

    关于更优雅实施的任何想法?

3 个答案:

答案 0 :(得分:3)

扩展我的评论。

既然你已经表示愿意沿着monad变形金刚的路线走下去,那么这应该是你想要的。不幸的是,由于斯卡拉在这里不太出色的类型测试,所以有很多线路噪音,但希望你发现它足够优雅。

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。
  • MonadPlus是由MonadPlus提供的操作。简而言之,正如其名称所暗示的那样,List(1, 2, 3) <+> List(4, 5, 6) = List(1, 2, 3, 4, 5, 6)捕捉到了直觉,即monad经常有一种直观的组合方式(例如findByInfo)。我们在Some(item)返回None时使用它来短路,而不是通常在List(item) <+> List() = List(item)上短路的行为(这大致类似于ItemDao.findByInfo)。

其他小注意事项,如果你真的想要沿着monad变形金刚的路线走下去,你经常会在你的monad变换器中构建一切(例如OptionT[Future, Item]会返回OptionT.apply),这样你就不会没有无关的.run电话,最后是//you'll of course want to get this via window.location.href instead: var exLoc = "http://www.somedomain.com/the-page/"; var root = "somedomain.com"; var end = exLoc.slice(exLoc.lastIndexOf(root)+root.length); //end is your "text" at the end of your domain (somedomain.com in this case) 一切。

答案 1 :(得分: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 }

答案 2 :(得分:1)

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

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。