Scala中的Future [Option]和for-comprehensions

37
我有两个返回 Future 的函数。我试图在使用 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",似乎我应该能够使用地图(map)来代替。

但是当我尝试使用地图(map):

  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个回答

22

关键在于 FutureOption 不能在 for 内部组合在一起,因为它们没有正确的 flatMap 签名。回想一下,for 的语法糖如下所示:

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--我只给出了一个例子--而等式语句只是在下一部分链之前设置变量)。由于你只能将其他Future进行flatMap,所以除了最后一个语句、、...,每个语句都必须产生一个Future

现在,getUserDetailsgetSchool都生成Futures,但sid是一个Option,所以我们不能将其放在<-的右侧。不幸的是,没有干净的开箱即用的方法来做到这一点。如果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-comprehension 看起来又合理了:

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

这是否是编写此代码的最佳方式?可能不是;它依靠将 None 转换为异常,仅因为你在那时不知道该做什么其他事情。由于 Future 的设计决策,这很难解决; 我建议您的原始代码(调用筛选器)至少是一种很好的方法来完成它。


1
这给我编译错误。找到:scala.concurrent.Future [Option [com.authorpub.userservice.School]]需要:Option [?] s <- schoolStore.getSchool(sid) - Ryan Bair
你能解释一下你所有的类型是什么吗?由于你没有发布完整的工作代码,所以有点难以确定。在你的工作代码中,future和option是什么?如果你这样做:import scala.reflect.runtime.universe._; def typeme[A: TypeTag](a: A) = { println(implicitly[TypeTag[A]]); a },你可以让Scala 2.10打印出表达式的类型,然后将表达式包装在typeme中,例如:sid = typeme(ud.right.toOption.flatMap(_.schoolID)) - Rex Kerr
2
有人没有留下评论就给我点了踩。这样并不是很有用。现在的回答有什么问题吗? - Rex Kerr
我更喜欢Ben James的答案。尽管Scalaz、Monads和所有这些东西让很多人感到害怕,但实际上这些概念非常简单。Scalaz已经拥有了解决问题所需的所有抽象。相反,这个答案引入了一个新的概念OptionIsFuture,我认为它类似于一个monad transformer。 - ps_ttf

14

这个回答对于一个关于Promise[Option[A]]的类似问题可能会有所帮助,只需要将Promise替换为Future即可。

从你的问题中推断出getUserDetailsgetSchool的类型如下:

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

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

一旦您获得了FutureMonad实例(在scalaz中可能有一个,或者像我链接的答案中一样编写您自己的实现),将OptionT转换器应用于您的问题将看起来像这样:

for {
  ud  <- optionT(getUserDetails(user.userID) map (_.right.toOption))
  sid <- optionT(Future.successful(ud.schoolID))
  s   <- optionT(getSchool(sid))
} yield s

请注意,为了保持类型兼容性,ud.schoolID被包装在一个(已经完成的)Future中。

这个for-comprehension的结果将具有类型OptionT[Future, SchoolID]. 您可以使用转换器的run方法提取类型为Future [Option [SchoolID]]的值。


2
诚然,Scalaz 有点吓人,我在学习曲线上还有很长的路要走。但这个解决方案已经解决了问题,我认为它做得很好。谢谢! - Ryan Bair
谢谢您的回答,但是 scalaz-contrib 库的链接已经失效。 - Luca Molteni
你不再需要 scalaz-contrib 了,因为现在通过混合 FutureInstances 特性,Monad 的 future 实例由 scalaz 自身提供。 - Luca Molteni

8
如果Option[School]None,您希望发生什么行为?您希望Future失败吗?使用哪种异常?您希望它永远不会完成吗?(听起来像个坏主意)。
无论如何,在for表达式中的if子句都被展开为对filter方法的调用。 Future#filter的合同如下:
如果当前future包含满足谓词的值,则新的future也将保留该值。否则,生成的future将因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]的实例。但你没有描述你想要其他的行为。


4
赞成不需要Scalaz的解决方案。;) 躲开 - iwein

3
我们对Future [Option [T]]进行了小包装,它的行为就像一个单子(甚至没有检查任何单子法则,但有map,flatMap,foreach,filter等) - MaybeLater。它的表现远不止是一个异步选项。
这里有很多臭代码,但至少作为一个示例可能会有用。 顺便说一句:还有很多未解决的问题(在这里)。

1

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接