【问题标题】:Future[Option] in Scala for-comprehensionsScala 中的 Future[Option] 用于理解
【发布时间】:2016-01-19 18:39:17
【问题描述】:

我有两个返回 Futures 的函数。我正在尝试使用 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
}

【问题讨论】:

    标签: scala future for-comprehension


    【解决方案1】:

    (已编辑以给出正确答案!)

    这里的关键是FutureOption 不要在for 内编写,因为没有正确的flatMap 签名。提醒一下,对于像这样的脱糖剂:

    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其他Futures,所以每个语句c0c1,...除了最后一个最好产生一个Future

    现在,getUserDetailsgetSchool 都产生 Futures,但 sidOption,所以我们不能把它放在 &lt;- 的右侧。不幸的是,没有干净的开箱即用的方法来做到这一点。如果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 的设计决策,这很难解决;我建议您的原始代码(调用过滤器)至少是一种很好的方法。

    【讨论】:

    • 这给了我一个编译错误。发现:scala.concurrent.Future[Option[com.authorpub.userservice.School]] 需要:Option[?] s
    • 你能解释一下你所有的类型是什么吗?鉴于您尚未发布完整的工作代码,这有点难以判断。什么是未来,你的工作代码中有什么选项?如果您这样做,您可以让 Scala 2.10 打印出表达式的类型:import scala.reflect.runtime.universe._; def typeme[A: TypeTag](a: A) = { println(implicitly[TypeTag[A]]); a },然后将表达式包装在 typeme 中,例如sid = typeme(ud.right.toOption.flatMap(_.schoolID)).
    • 有人在没有评论的情况下投了反对票。这不是很有用。现在的答案有什么问题?
    • 我更喜欢 Ben James 的回答。尽管 Scalaz、Monads 和所有这些东西让很多人感到害怕,但实际上这些概念非常简单。 Scalaz 已经拥有解决问题所需的所有抽象。相反,这个答案引入了一个新概念OptionIsFuture,我想它类似于一个单子变压器。
    【解决方案2】:

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

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

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

    由于您忽略了来自Either 的失败值,而是将其转换为Option,因此您实际上有两个A =&gt; 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 中。

    这种理解的结果将具有类型OptionT[Future, SchoolID]。您可以使用转换器的run 方法提取Future[Option[SchoolID]] 类型的值。

    【讨论】:

    • 诚然 scalaz 让我有点害怕,但我还有很长的路要走。这解决了我认为可以解决的问题。谢谢!
    • 感谢您的回答,但指向 scalaz-contrib 库的链接已损坏
    • 你不需要 scalaz-contrib anymore,因为 Monad 的未来实例现在由 scalaz 本身通过混合 FutureInstances 特征提供。
    【解决方案3】:

    如果Option[School]None,您希望发生什么行为?你希望未来失败吗?有什么样的例外?你希望它永远不会完成吗? (这听起来是个坏主意)。

    无论如何,for 表达式中的 if 子句取消了对 filter 方法的调用。 Future#filter上的合约是这样的:

    如果当前未来包含满足谓词的值, 新的未来也将保持这种价值。否则,结果 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] 的实例。但你还没有描述你想要的其他行为。

    【讨论】:

    • +1 表示不需要 Scalaz 的解决方案;) ducks
    【解决方案4】:

    我们在 Future[Option[T]] 上做了一个小的包装器,它就像一个 monad(甚至没有人检查过任何 monad 法则,但是有 map、flatMap、foreach、filter 等等)-MaybeLater。它的行为远不止异步选项。

    那里有很多臭代码,但至少作为示例可能会有用。 顺便说一句:有很多未解决的问题(例如here。)

    【讨论】:

      【解决方案5】:

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

      【讨论】:

        猜你喜欢
        • 2016-01-02
        • 2013-02-15
        • 2015-10-09
        • 2019-05-25
        • 2016-11-08
        • 1970-01-01
        • 2014-11-19
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多