【问题标题】:Scala for comprehension with Future, List and OptionScala 用于理解 Future、List 和 Option
【发布时间】:2016-01-02 13:37:14
【问题描述】:

我在 Scala 和 Play 框架中构建一个响应式站点,我的数据模型是这样的,我经常需要编写 FutureOption,并从以前的 List / Set 构建 Future值来获得我需要的结果。

我编写了一个简单的应用程序,其中包含一个可以复制和粘贴的假数据源,它应该可以编译。我的问题是,在我的情况下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
}

【问题讨论】:

  • 我不确定你是否需要嵌套收益

标签: scala playframework future reactivemongo


【解决方案1】:

您创建了FutureO,它结合了FutureOption 的效果(如果您正在研究Scalaz,则与OptionT[Future, ?] 进行比较)。

记住for ... yield 类似于FutureO.map,结果类型将始终为FutureO[?](如果您使用Future[Option[?]],则为Future[Option[?]])。

问题是您想返回 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]

如果您有多个函数返回Future[Option[?]],您可能喜欢使用FutureO,在这种情况下您可以创建一个额外的函数Future[A] =&gt; 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]

但正如您所见,您总是需要处理“选项上下文”才能返回Future[UserContext]

【讨论】:

  • 谢谢彼得!我喜欢将一切标准化为 FutureO 的第二种方法。实际上,我确实有更多涉及多个 FutureO 的用例,因此该解决方案将完美运行。感谢您提前考虑并提出建议!
【解决方案2】:

为了扩展 Peter Neyens 的回答,我通常会将一堆 monad -> monad 转换放在一个特殊的隐式类中,并在需要时导入它们。这里我们有两个 monad,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"

【讨论】:

  • 非常感谢丹尼尔的建议!将 monad 转换隐式化并分组到一个类中是一个很好的改进。
  • 这里可能有语法错误吗?对于implicit def optionOps[T]... 属于哪个范围以及它与@peter-neyens 答案的关系,我有点困惑。在这里向 Scala 新手道歉。
  • foo.optionOps 导入包中会将toFuture 暴露给任何Option[T]bOption[Any],所以 b.toFuture 产生 Future[Any]。这只是通过将选项转换为未来来扁平化Future[Option[Future[T]]] 的不同解决方案。
猜你喜欢
  • 2013-02-15
  • 1970-01-01
  • 2015-10-09
  • 1970-01-01
  • 1970-01-01
  • 2016-11-08
  • 1970-01-01
  • 2019-05-25
  • 1970-01-01
相关资源
最近更新 更多