Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 728
John Doe Avatar asked Sep 16 '16 11:09

John Doe


3 Answers

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.

like image 105
Ori Popowski Avatar answered Sep 21 '22 21:09

Ori Popowski


Let's take a step back and explore the meaning of our expression:

  • A Future is "eventually a value (but might fail)"
  • An 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 x
  • Success(None) => Finished but got nothing => This is probably an application-level error
  • Failure(_) => Something went wrong, so we don't have a value

We 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")
}
like image 35
maasg Avatar answered Sep 20 '22 21:09

maasg


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
like image 26
pamu Avatar answered Sep 20 '22 21:09

pamu