Scala的for推导式用于Futures和Options

7
我最近读了曼努埃尔·伯恩哈特的新书《响应式Web应用程序》。在他的书中,他说Scala开发者永远不要使用.get来检索可选值。
我想采纳他的建议,但是当我在Futures中使用for循环时,我很难避免使用.get
假设我有以下代码:
for {
        avatarUrl <- avatarService.retrieve(email)
        user <- accountService.save(Account(profiles = List(profile.copy(avatarUrl = avatarUrl)))
        userId <- user.id
        _ <- accountTokenService.save(AccountToken.create(userId, email))
      } yield {
        Logger.info("Foo bar")
      }

通常情况下,我会使用AccountToken.create(user.id.get, email)而不是AccountToken.create(userId, email)。然而,当试图避免这种不良实践时,我会得到以下异常:

[error]  found   : Option[Nothing]
[error]  required: scala.concurrent.Future[?]
[error]         userId <- user.id
[error]                ^

我该如何解决这个问题?


for-推导式的问题在于它们总是需要相同的单子。也就是说,您不能使用将OptionFuture组合的for-推导式 :( - irundaia
啊,好的!谢谢你的帮助。你能推荐一些优雅的解决方案来避免使用.get吗? - John Doe
类似 userId <- user.id.map(concurrent.Future.successful(_) ).getOrElse(concurrent.Future.failed(new Exception(...))) 这样的代码可能会有所帮助。 - Victor Moroz
3个回答

7

第一种选择

如果你真的想使用for推导式,你需要将其分解为多个for,每个for都必须使用相同的单子类型:

for {
  avatarUrl <- avatarService.retrieve(email)
  user <- accountService.save(Account(profiles = List(profile.copy(avatarUrl = avatarUrl)))
} yield for {
  userId <- user.id
} yield for {
  _ <- accountTokenService.save(AccountToken.create(userId, email))
}

第二个选择

另一种选择是完全避免使用 Future[Option[T]],而使用 Future[T],它可以转化为 Failure(e),其中 eNoSuchElementException,每当您期望一个 None 时(在您的情况下,是 accountService.save() 方法):

def saveWithoutOption(account: Account): Future[User] = {
  this.save(account) map { userOpt =>
    userOpt.getOrElse(throw new NoSuchElementException)
  }
}

然后您将拥有:
(for {
  avatarUrl <- avatarService.retrieve(email)
  user <- accountService.saveWithoutOption(Account(profiles = List(profile.copy(avatarUrl = avatarUrl)))
  _ <- accountTokenService.save(AccountToken.create(user.id, email))
} yield {
  Logger.info("Foo bar")
}) recover {
  case t: NoSuchElementException => Logger.error("boo")
}

第三种选择

退而求其次,使用map/flatMap并引入中间结果。


3
让我们退后一步,探索一下我们表达式的含义:
- `Future` 是“最终是一个值(但可能失败)” - `Option` 是“可能是一个值”
那么 `Future[Option]` 的语义是什么呢?让我们通过探索这些值来获得一些直觉:
`Future[Option]`
- `Success(Some(x))` => 好的。让我们使用 x 进行一些操作。 - `Success(None)` => 完成了但没有得到任何东西 => 这可能是应用程序级别的错误。 - `Failure(_)` => 出了问题,所以我们没有值。
我们可以将 `Success(None)` 扁平化为 `Failure(SomeApplicationException)` 并消除单独处理 `Option` 的需要。
为此,我们可以定义一个通用函数将 `Option` 转换为 `Future`,并使用 `for-comprehension` 应用扁平化。
def optionToFuture[T](opt:Option[T], ex: ()=>Exception):Future[T] = opt match {
   case Some(v) => Future.successful(v)
   case None => Future.failed(ex())
  }

我们现在可以通过一个for-comprehension来流畅地表达我们的计算:
for {
  avatarUrl <- avatarService.retrieve(email)
  user <- accountService.save(Account(profiles = List(profile.copy(avatarUrl = avatarUrl)))
  userId <- optionToFuture(user.id, () => new UserNotFoundException(email))
  _ <- accountTokenService.save(AccountToken.create(userId, email))
} yield {
   Logger.info("Foo bar")
}

我喜欢那个。谢谢! - John Doe

1
停止通过将Future失败来传播选项,当选项为None时。
当id为None时,使Future失败并中止。
for {
....
accountOpt <-
  user.id.map { id =>
    Account.create(id, ...)
  }.getOrElse {
   Future.failed(new Exception("could not create account."))
  }

...
} yield result

最好有一个自定义异常,例如:
case class NoIdException(msg: String) extends Exception(msg)

在Option上调用.get应该只在您确定选项是Some(x)时才执行,否则.get将抛出异常。

使用.get不是好习惯,因为它可能会在代码中引发异常。

取而代之的是,使用getOrElse是一个好习惯。

您可以使用mapflatMap选项来访问内部值。

好的实践

val x: Option[Int] = giveMeOption()
x.getOrElse(defaultValue)

可以在这里使用Get

val x: Option[Int] = giveMeOption()
x.OrElse(Some(1)).get

谢谢,但我已经知道了。我认为你的回答并没有解决我的问题... - John Doe
@JohnDoe,我认为这将帮助你摆脱选项。 - Nagarjuna Pamu

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