用于理解 Future、List 和 Option 的 Scala
Scala for comprehension with Future, List and Option
我正在用 Scala 和 Play Framework 构建一个反应式站点,我的数据模型经常需要组合 Future
和 Option
,并构建 Future
List
/ Set
从以前的值得到我需要的结果。
我用一个假数据源写了一个简单的应用程序,你可以复制和粘贴它应该编译。我的问题是,在我的例子 UserContext
中,如何以可消耗的形式返回结果。目前,我正在返回 Future[Option[Future[UserContext]]]
。
我想在纯 Scala 中做这件事以更好地学习这门语言,所以我目前正在避免使用 Scalaz。虽然我知道我最终应该使用那个。
package futures
import scala.concurrent.{Future, ExecutionContext}
// http://www.edofic.com/posts/2014-03-07-practical-future-option.html
case class FutureO[+A](future: Future[Option[A]]) extends AnyVal {
def flatMap[B](f: A => FutureO[B])(implicit ec: ExecutionContext): FutureO[B] = {
FutureO {
future.flatMap { optA =>
optA.map { a =>
f(a).future
} getOrElse Future.successful(None)
}
}
}
def map[B](f: A => B)(implicit ec: ExecutionContext): FutureO[B] = {
FutureO(future.map(_ map f))
}
}
// ========== USAGE OF FutureO BELOW ============= \
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
object TeamDB {
val basketballTeam = Team(id = 111, player_ids = Set(111, 222))
val baseballTeam = Team(id = 222, player_ids = Set(333))
def findById(teamId: Int): Future[Option[Team]] = Future.successful(
teamId match {
case 111 => Some(basketballTeam)
case 222 => Some(baseballTeam)
case _ => None
}
)
}
object PlayerDB {
val basketballPlayer1 = Player(id = 111, jerseyNumber = 23)
val basketballPlayer2 = Player(id = 222, jerseyNumber = 45)
val baseballPlayer = Player(id = 333, jerseyNumber = 5)
def findById(playerId: Int): Future[Option[Player]] = Future.successful(
playerId match {
case 111 => Some(basketballPlayer1)
case 222 => Some(basketballPlayer2)
case 333 => Some(baseballPlayer)
case _ => None
}
)
}
object UserDB {
// user1 is on BOTH the baseball and basketball team
val user1 = User(id = 111, name = "Michael Jordan", player_ids = Set(111, 333), team_ids = Set(111, 222))
// user2 is ONLY on the basketball team
val user2 = User(id = 222, name = "David Wright", player_ids = Set(222), team_ids = Set(111))
def findById(userId: Long): Future[Option[User]] = Future.successful(
userId match {
case 111 => Some(user1)
case 222 => Some(user2)
case _ => None
}
)
}
case class User(id: Int, name: String, player_ids: Set[Int], team_ids: Set[Int])
case class Player(id: Int, jerseyNumber: Int)
case class Team(id: Int, player_ids: Set[Int])
case class UserContext(user: User, teams: Set[Team], players: Set[Player])
object FutureOptionListTest extends App {
val result = for {
user <- FutureO(UserDB.findById(userId = 111))
} yield for {
players: Set[Option[Player]] <- Future.traverse(user.player_ids)(x => PlayerDB.findById(x))
teams: Set[Option[Team]] <- Future.traverse(user.team_ids)(x => TeamDB.findById(x))
} yield {
UserContext(user, teams.flatten, players.flatten)
}
result.future // returns Future[Option[Future[UserContext]]] but I just want Future[UserContext] or UserContext
}
您创建了 FutureO
,它结合了 Future
和 Option
的效果(如果您正在研究 Scalaz,则将其与 OptionT[Future, ?]
进行比较)。
请记住 for ... yield
类似于 FutureO.map
,结果类型将始终为 FutureO[?]
(如果您执行 result.future
,则为 Future[Option[?]]
)。
问题是您想要 return Future[UserContex]
而不是 Future[Option[UserContext]]
。本质上你想要松开 Option
上下文,所以你需要在某处显式处理用户是否存在。
在这种情况下,一个可能的解决方案是省略 FutureO
,因为您只使用它一次。
case class NoUserFoundException(id: Long) extends Exception
// for comprehension with Future
val result = for {
user <- UserDB.findById(userId = 111) flatMap (
// handle Option (Future[Option[User]] => Future[User])
_.map(user => Future.successful(user))
.getOrElse(Future.failed(NoUserFoundException(111)))
)
players <- Future.traverse(user.player_ids)(x => PlayerDB.findById(x))
teams <- Future.traverse(user.team_ids)(x => TeamDB.findById(x))
} yield UserContext(user, teams.flatten, players.flatten)
// result: scala.concurrent.Future[UserContext]
如果你有多个函数return一个Future[Option[?]]
,你可能会喜欢使用FutureO
,在这种情况下你可以创建一个额外的函数 Future[A] => FutureO[A]
,这样你就可以在相同的 for
理解中使用你的函数(全部在 FutureO
monad 中):
def liftFO[A](fut: Future[A]) = FutureO(fut.map(Some(_)))
// for comprehension with FutureO
val futureO = for {
user <- FutureO(UserDB.findById(userId = 111))
players <- liftFO(Future.traverse(user.player_ids)(x => PlayerDB.findById(x)))
teams <- liftFO(Future.traverse(user.team_ids)(x => TeamDB.findById(x)))
} yield UserContext(user, teams.flatten, players.flatten)
// futureO: FutureO[UserContext]
val result = futureO.future flatMap (
// handle Option (Future[Option[UserContext]] => Future[UserContext])
_.map(user => Future.successful(user))
.getOrElse(Future.failed(new RuntimeException("Could not find UserContext")))
)
// result: scala.concurrent.Future[UserContext]
但是如您所见,您总是需要先处理 "option context",然后才能 return 一个 Future[UserContext]
。
为了扩展 Peter Neyens 的回答,我通常会将一堆 monad -> monad 转换放在一个特殊的隐式 class 中,并在需要时导入它们。这里我们有两个单子,Option[T]
和 Future[T]
。在这种情况下,您将 None
视为失败的 Future
。你可能会这样做:
package foo {
class OptionOps[T](in: Option[T]) {
def toFuture: Future[T] = in match {
case Some(t) => Future.successful(t)
case None => Future.failed(new Exception("option was none"))
}
}
implicit def optionOps[T](in: Option[T]) = new OptionOps[T](in)
}
然后你只需要导入它import foo.optionOps
然后:
val a: Future[Any] = ...
val b: Option[Any] = Some("hi")
for {
aFuture <- a
bFuture <- b.toFuture
} yield bFuture // yields a successful future containing "hi"
我正在用 Scala 和 Play Framework 构建一个反应式站点,我的数据模型经常需要组合 Future
和 Option
,并构建 Future
List
/ Set
从以前的值得到我需要的结果。
我用一个假数据源写了一个简单的应用程序,你可以复制和粘贴它应该编译。我的问题是,在我的例子 UserContext
中,如何以可消耗的形式返回结果。目前,我正在返回 Future[Option[Future[UserContext]]]
。
我想在纯 Scala 中做这件事以更好地学习这门语言,所以我目前正在避免使用 Scalaz。虽然我知道我最终应该使用那个。
package futures
import scala.concurrent.{Future, ExecutionContext}
// http://www.edofic.com/posts/2014-03-07-practical-future-option.html
case class FutureO[+A](future: Future[Option[A]]) extends AnyVal {
def flatMap[B](f: A => FutureO[B])(implicit ec: ExecutionContext): FutureO[B] = {
FutureO {
future.flatMap { optA =>
optA.map { a =>
f(a).future
} getOrElse Future.successful(None)
}
}
}
def map[B](f: A => B)(implicit ec: ExecutionContext): FutureO[B] = {
FutureO(future.map(_ map f))
}
}
// ========== USAGE OF FutureO BELOW ============= \
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
object TeamDB {
val basketballTeam = Team(id = 111, player_ids = Set(111, 222))
val baseballTeam = Team(id = 222, player_ids = Set(333))
def findById(teamId: Int): Future[Option[Team]] = Future.successful(
teamId match {
case 111 => Some(basketballTeam)
case 222 => Some(baseballTeam)
case _ => None
}
)
}
object PlayerDB {
val basketballPlayer1 = Player(id = 111, jerseyNumber = 23)
val basketballPlayer2 = Player(id = 222, jerseyNumber = 45)
val baseballPlayer = Player(id = 333, jerseyNumber = 5)
def findById(playerId: Int): Future[Option[Player]] = Future.successful(
playerId match {
case 111 => Some(basketballPlayer1)
case 222 => Some(basketballPlayer2)
case 333 => Some(baseballPlayer)
case _ => None
}
)
}
object UserDB {
// user1 is on BOTH the baseball and basketball team
val user1 = User(id = 111, name = "Michael Jordan", player_ids = Set(111, 333), team_ids = Set(111, 222))
// user2 is ONLY on the basketball team
val user2 = User(id = 222, name = "David Wright", player_ids = Set(222), team_ids = Set(111))
def findById(userId: Long): Future[Option[User]] = Future.successful(
userId match {
case 111 => Some(user1)
case 222 => Some(user2)
case _ => None
}
)
}
case class User(id: Int, name: String, player_ids: Set[Int], team_ids: Set[Int])
case class Player(id: Int, jerseyNumber: Int)
case class Team(id: Int, player_ids: Set[Int])
case class UserContext(user: User, teams: Set[Team], players: Set[Player])
object FutureOptionListTest extends App {
val result = for {
user <- FutureO(UserDB.findById(userId = 111))
} yield for {
players: Set[Option[Player]] <- Future.traverse(user.player_ids)(x => PlayerDB.findById(x))
teams: Set[Option[Team]] <- Future.traverse(user.team_ids)(x => TeamDB.findById(x))
} yield {
UserContext(user, teams.flatten, players.flatten)
}
result.future // returns Future[Option[Future[UserContext]]] but I just want Future[UserContext] or UserContext
}
您创建了 FutureO
,它结合了 Future
和 Option
的效果(如果您正在研究 Scalaz,则将其与 OptionT[Future, ?]
进行比较)。
请记住 for ... yield
类似于 FutureO.map
,结果类型将始终为 FutureO[?]
(如果您执行 result.future
,则为 Future[Option[?]]
)。
问题是您想要 return Future[UserContex]
而不是 Future[Option[UserContext]]
。本质上你想要松开 Option
上下文,所以你需要在某处显式处理用户是否存在。
在这种情况下,一个可能的解决方案是省略 FutureO
,因为您只使用它一次。
case class NoUserFoundException(id: Long) extends Exception
// for comprehension with Future
val result = for {
user <- UserDB.findById(userId = 111) flatMap (
// handle Option (Future[Option[User]] => Future[User])
_.map(user => Future.successful(user))
.getOrElse(Future.failed(NoUserFoundException(111)))
)
players <- Future.traverse(user.player_ids)(x => PlayerDB.findById(x))
teams <- Future.traverse(user.team_ids)(x => TeamDB.findById(x))
} yield UserContext(user, teams.flatten, players.flatten)
// result: scala.concurrent.Future[UserContext]
如果你有多个函数return一个Future[Option[?]]
,你可能会喜欢使用FutureO
,在这种情况下你可以创建一个额外的函数 Future[A] => FutureO[A]
,这样你就可以在相同的 for
理解中使用你的函数(全部在 FutureO
monad 中):
def liftFO[A](fut: Future[A]) = FutureO(fut.map(Some(_)))
// for comprehension with FutureO
val futureO = for {
user <- FutureO(UserDB.findById(userId = 111))
players <- liftFO(Future.traverse(user.player_ids)(x => PlayerDB.findById(x)))
teams <- liftFO(Future.traverse(user.team_ids)(x => TeamDB.findById(x)))
} yield UserContext(user, teams.flatten, players.flatten)
// futureO: FutureO[UserContext]
val result = futureO.future flatMap (
// handle Option (Future[Option[UserContext]] => Future[UserContext])
_.map(user => Future.successful(user))
.getOrElse(Future.failed(new RuntimeException("Could not find UserContext")))
)
// result: scala.concurrent.Future[UserContext]
但是如您所见,您总是需要先处理 "option context",然后才能 return 一个 Future[UserContext]
。
为了扩展 Peter Neyens 的回答,我通常会将一堆 monad -> monad 转换放在一个特殊的隐式 class 中,并在需要时导入它们。这里我们有两个单子,Option[T]
和 Future[T]
。在这种情况下,您将 None
视为失败的 Future
。你可能会这样做:
package foo {
class OptionOps[T](in: Option[T]) {
def toFuture: Future[T] = in match {
case Some(t) => Future.successful(t)
case None => Future.failed(new Exception("option was none"))
}
}
implicit def optionOps[T](in: Option[T]) = new OptionOps[T](in)
}
然后你只需要导入它import foo.optionOps
然后:
val a: Future[Any] = ...
val b: Option[Any] = Some("hi")
for {
aFuture <- a
bFuture <- b.toFuture
} yield bFuture // yields a successful future containing "hi"