John Doe John Doe - 2 months ago 10
Scala Question

Scala's for comprehension for Futures and Options

I have recently read Manuel Bernhardt's new book Reactive Web Applications. In his book, he states that Scala developers should never use

.get
to retrieve an optional value.

I want to pick up his suggestions but I am struggling to avoid
.get
when using for comprehensions for Futures.

Let's say I have the following code:

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")
}


Normally, I would have used
AccountToken.create(user.id.get, email)
instead of
AccountToken.create(userId, email)
. However, when trying to avoid this bad practice, I get the following exception:

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


How can I solve this?

Answer

First option

If you really want to use for comprehension you'll have to separate it to several fors, where each works with the same monad type:

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))
}

Second option

Another option is to avoid Future[Option[T]] altogether and use Future[T] which can materialize into Failure(e) where e is a NoSuchElementException whenever you expect a None (in your case, the accountService.save() method):

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

Then you'll have:

(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")
}

Third option

Fall back to map/flatMap and introduce intermediate results.