我有一个管理Item
的应用。当客户端通过某个info
查询项目时,应用程序首先尝试使用信息在数据库中查找现有项目。如果没有,那么应用
检查info
是否有效。这是一项昂贵的操作(比db查找要多得多),因此应用程序仅在数据库中没有现有项目时执行此操作。
如果info
有效,请使用Item
将新info
插入数据库。
还有两个课程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
的{{1}}进行清理,但非可选的OptionT
也不是很容易。
关于更优雅实施的任何想法?
答案 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。