我有一个管理Item
s的应用程序。当客户端通过某个info
查询项目时,该应用程序首先尝试在数据库中查找包含该信息的现有项目。如果没有,该应用程序将
-
检查
info
是否有效。这是一个昂贵的操作(比数据库查找要贵得多(,因此应用程序只有在数据库中没有现有项目时才会执行此操作。 -
如果
info
有效,则使用info
在数据库中插入一个新的Item
。
还有两个类,ItemDao
和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)
}
}
}
}
ItemService.findByInfo(info: Info)
方法非常难看。我已经尝试清理它一段时间了,但这很困难,因为涉及到三种类型(Future[Boolean]
、Future[Item]
和Future[Option[Item]]
(。我尝试过使用scalaz
的OptionT
来清理它,但非可选的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的东西。