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 for
s, 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