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?
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))
}
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")
}
Fall back to map/flatMap and introduce intermediate results.
Let's take a step back and explore the meaning of our expression:
Future is "eventually a value (but might fail)"Option is "maybe a value"What are the semantics of Future[Option]? Let's explore the values to gain some intuition:
Future[Option]
Success(Some(x)) => Good. Let's do stuff with xSuccess(None) => Finished but got nothing => This is probably an application-level errorFailure(_) => Something went wrong, so we don't have a valueWe can flatten Success(None) into a Failure(SomeApplicationException) and eliminate the need of handling the Option separately.
For that, we can define a generic function to turn an Option into a Future and use the for-comprehension to apply the flattening.
def optionToFuture[T](opt:Option[T], ex: ()=>Exception):Future[T] = opt match {
case Some(v) => Future.successful(v)
case None => Future.failed(ex())
}
We can now express our computation fluently with a 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")
}
Stop Option propogation by failing the Future when option is None
Fail the future when id is none and abort
for {
....
accountOpt <-
user.id.map { id =>
Account.create(id, ...)
}.getOrElse {
Future.failed(new Exception("could not create account."))
}
...
} yield result
Better to have a custom exception like
case class NoIdException(msg: String) extends Exception(msg)
invoking .get on Option should be done only if you are sure that option is Some(x) otherwise .get will throw an exception.
Thats by using .get is not good practise because it may cause an exception in the code.
Instead of .get its good practice to use getOrElse.
You can map or flatMap the option to get access to the inner value.
Good practice
val x: Option[Int] = giveMeOption()
x.getOrElse(defaultValue)
Get can be used here
val x: Option[Int] = giveMeOption()
x.OrElse(Some(1)).get
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With