首页 文章

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

提问于
浏览
34

我有两个返回期货的功能 . 我试图使用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 回答

  • 1

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

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

  • 8

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

  • 21

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

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

    如果当前的未来包含满足谓词的值,则新的未来也将保持该值 . 否则,结果将来会因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] 的实例 . 但是你还没有喜欢 .

  • 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]] 的值 .

    一旦你得到 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]] 类型的值 .

  • 2

    (编辑给出正确答案!)

    这里的关键是 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 其他 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 的设计决定,这很难解决;我建议您的原始代码(调用过滤器)至少是一种方法 .

相关问题