Scala for-comprehensions中的未来[选项]

时间:2013-01-17 18:23:41

标签: scala future for-comprehension

我有两个返回期货的功能。我试图使用for-yield理解将第一个函数的修改结果输入到另一个函数中。

这种方法有效:

  val schoolFuture = for {
    ud <- userStore.getUserDetails(user.userId)
    sid = ud.right.toOption.flatMap(_.schoolId)
    s <- schoolStore.getSchool(sid.get) if sid.isDefined
  } yield s

但是我对那里的“if”感到不满意,似乎我应该可以使用地图了。

但是当我尝试使用地图时:

  val schoolFuture: Future[Option[School]] = for {
    ud <- userStore.getUserDetails(user.userId)
    sid = ud.right.toOption.flatMap(_.schoolId)
    s <- sid.map(schoolStore.getSchool(_))
  } yield s

我收到编译错误:

[error]  found   : Option[scala.concurrent.Future[Option[School]]]
[error]  required: scala.concurrent.Future[Option[School]]
[error]         s <- sid.map(schoolStore.getSchool(_))

我玩了几个变种,但没有发现任何有吸引力的作品。任何人都可以建议更好的理解和/或解释我的第二个例子有什么问题吗?

这是Scala 2.10的最小但完整的可运行示例:

import concurrent.{Future, Promise}

case class User(userId: Int)
case class UserDetails(userId: Int, schoolId: Option[Int])
case class School(schoolId: Int, name: String)

trait Error

class UserStore {
  def getUserDetails(userId: Int): Future[Either[Error, UserDetails]] = Promise.successful(Right(UserDetails(1, Some(1)))).future
}

class SchoolStore {
  def getSchool(schoolId: Int): Future[Option[School]] = Promise.successful(Option(School(1, "Big School"))).future
}

object Demo {
  import concurrent.ExecutionContext.Implicits.global

  val userStore = new UserStore
  val schoolStore = new SchoolStore

  val user = User(1)

  val schoolFuture: Future[Option[School]] = for {
    ud <- userStore.getUserDetails(user.userId)
    sid = ud.right.toOption.flatMap(_.schoolId)
    s <- sid.map(schoolStore.getSchool(_))
  } yield s
}

5 个答案:

答案 0 :(得分:21)

(编辑给出正确答案!)

此处的关键是FutureOption 不在for 内撰写,因为没有正确的flatMap签名。提醒一下,对于这样的desugars:

for ( x0 <- c0; w1 = d1; x1 <- c1 if p1; ... ; xN <- cN) yield f
c0.flatMap{ x0 => 
  val w1 = d1
  c1.filter(x1 => p1).flatMap{ x1 =>
    ... cN.map(xN => f) ... 
  }
}

(其中任何if语句将filter抛出到链中 - 我只给出了一个例子 - 而equals语句只是在链的下一部分之前设置变量)。由于您只能flatMap其他Future个,所以每个语句c0c1,...除了最后一个之外最好生成Future

现在,getUserDetailsgetSchool都生成Futures,但sidOption,所以我们不能把它放在右边<-的一面。不幸的是,没有干净的开箱即用的方法来做到这一点。如果o是一个选项,我们可以

o.map(Future.successful).getOrElse(Future.failed(new Exception))

Option变为已完成的Future。所以

for {
  ud <- userStore.getUserDetails(user.userId)  // RHS is a Future[Either[...]]
  sid = ud.right.toOption.flatMap(_.schoolId)  // RHS is an Option[Int]
  fid <- sid.map(Future.successful).getOrElse(Future.failed(new Exception))  // RHS is Future[Int]
  s <- schoolStore.getSchool(fid)
} yield s

会做到这一点。这比你得到的更好吗?疑。但是如果你

implicit class OptionIsFuture[A](val option: Option[A]) extends AnyVal {
  def future = option.map(Future.successful).getOrElse(Future.failed(new Exception))
}
然后突然之间的理解再次看起来合情合理:

for {
  ud <- userStore.getUserDetails(user.userId)
  sid <- ud.right.toOption.flatMap(_.schoolId).future
  s <- schoolStore.getSchool(sid)
} yield s

这是编写此代码的最佳方式吗?可能不是;它依赖于将None转换为异常仅仅是因为您不知道此时还要做什么。由于Future的设计决定,这很难解决;我建议您的原始代码(调用过滤器)至少是一种方法。

答案 1 :(得分:13)

This answer关于Promise[Option[A]]的类似问题可能有所帮助。只需将Future替换为Promise

我从您的问题中推断出getUserDetailsgetSchool的以下类型:

getUserDetails: UserID => Future[Either[??, UserDetails]]
getSchool: SchoolID => Future[Option[School]]

由于您忽略Either中的失败值,而是将其转换为Option,因此您实际上有两个A => Future[Option[B]]类型的值。

一旦MonadFuture个实例(scalaz中可能有一个,或者您可以在我链接的答案中编写自己的实例),请应用{ {1}}您的问题的变换器看起来像这样:

OptionT

请注意,为了保持类型兼容,for { ud <- optionT(getUserDetails(user.userID) map (_.right.toOption)) sid <- optionT(Future.successful(ud.schoolID)) s <- optionT(getSchool(sid)) } yield s 包含在(已完成的)Future中。

这种理解的结果将是ud.schoolID类型。您可以使用变换器的OptionT[Future, SchoolID]方法提取Future[Option[SchoolID]]类型的值。

答案 2 :(得分:8)

如果Option[School]None,您希望采取什么行为?你希望未来失败吗?有什么样的例外?你想要它永远不会完成吗? (这听起来不错)。

无论如何,for-expression中的if子句去了对filter方法的调用。因此Future#filter上的合同是:

  

如果当前未来包含满足谓词的值,   新的未来也将保持这一价值。否则,结果   未来将因NoSuchElementException而失败。

但等等:

scala> None.get
java.util.NoSuchElementException: None.get

如您所见,None.get返回的完全相同。

因此,摆脱if sid.isDefined应该有效,这应该会返回一个合理的结果:

  val schoolFuture = for {
    ud <- userStore.getUserDetails(user.userId)
    sid = ud.right.toOption.flatMap(_.schoolId)
    s <- schoolStore.getSchool(sid.get)
  } yield s

请注意,schoolFuture的结果可以是scala.util.Failure[NoSuchElementException]的实例。但是你还没有描述你想要的其他行为。

答案 3 :(得分:2)

我们在Future [Option [T]]上创建了一个小包装器,它就像一个monad(没有人甚至没有检查monad定律,但有map,flatMap,foreach,filter等) - {{3} }。它的行为远不止异步选项。

那里有很多有臭味的代码,但也许它至少可以作为一个例子。 顺便说一句:有很多未解决的问题(例如MaybeLater

答案 4 :(得分:1)

使用https://github.com/qifun/stateless-futurehttps://github.com/scala/async进行A-Normal-Form转换更容易。