Scala Future[A] 和 Future[Option[B]] 组合
Scala Future[A] and Future[Option[B]] composition
我有一个管理 Item
的应用程序。当客户端通过某些 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
也让它变得不太容易。
关于更优雅的实现有什么想法吗?
如果您对依赖于路径的类型和更高级的类型感到满意,那么下面的内容可能是一个优雅的解决方案:
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 }
我有一个管理 Item
的应用程序。当客户端通过某些 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
也让它变得不太容易。
关于更优雅的实现有什么想法吗?
如果您对依赖于路径的类型和更高级的类型感到满意,那么下面的内容可能是一个优雅的解决方案:
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
returnsSome(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 }