flybonzai
flybonzai

Reputation: 3951

Chaining together operations on an Option to a Future, then back to an Option?

I'm writing an authentication client that takes an Option[Credentials] as a parameter. This Credentials object has a .token method on it which I will then use to construct an HTTP request to post to an endpoint. This returns a Future[HttpResponse], which I then need to validate, unmarshal, and then convert back to my return type, which is an Option[String].

My first thought was to use a for comprehension like this:

val resp = for {
  c <- creds
  req <- buildRequest(c.token)
  resp <- Http().singleRequest(req)
} yield resp

but then I found out that monads cannot be composed like that. My next thought is to do something like this:

val respFut = Http().singleRequest(buildRequest(token))

    respFut.onComplete {
      case Success(resp) => Some("john.doe")//do stuff
      case Failure(_) => None
    }

Unfortunately onComplete returns a unit, and map leaves me with a Future[Option[String]], and the only way I currently know to strip off the future wrapper is using the pipeTo methods in the akka framework. How can I convert this back to just an option string?

Upvotes: 0

Views: 107

Answers (3)

Grant BlahaErath
Grant BlahaErath

Reputation: 2690

I'm not 100% certain about what all your types are, but it seems like you want a for comprehension that mixes option and futures. I've often been in that situation and I find I can just chain my for comprehensions as a way to make the code look a bit better.

val resp = for {
  c <- creds
  req <- buildRequest(c.token)
} yield for {
  resp <- Http().singleRequest(req)
} yield resp

resp becomes an Option[Future[HttpResponse]] which you can match / partial func around with None meaning the code never got to execute because it failed its conditions. This is a dumb little trick I use to make comprehensions look better and I hope it gives you a hint towards your solution.

Upvotes: 1

Alvaro Carrasco
Alvaro Carrasco

Reputation: 6182

It's easy to "upgrade" an Option to a Future[Option[...]], so use Future as your main monad. And deal with the simpler case first:

val f: Future[Option[String]] = 
  // no credential? just wrap a `None` in a successful future
  credsOpt.fold(Future.successful(Option.empty[String])) {creds =>
    Http()
      .singleRequest(buildRequest(creds.token))
      .map(convertResponseToString)
      .recover {case _ => Option.empty[String]}
  }

The only way to turn that future into Option[String] is to wait for it with Await.result(...)... but it's better if that future can be passed along to the next caller (no blocking).

Upvotes: 1

Joe K
Joe K

Reputation: 18434

Once you've got a Future[T], it's usually good practice to not try to unbox it until you absolutely have to. Can you change your method to return a Future[Option[String]]? How far up the call stack can you deal with futures? Ideally it's all the way.

Something like this will give you a Future[Option[String]] as a result:

val futureResult = creds map {
  case Some(c) => {
    val req = buildRequest(c.token)
    val futureResponse = Http().singleRequest(req)
    futureResponse.map(res => Some(convertResponseToString(res)))
  }
  case None => Future(None)
}

If you really need to block and wait on the result, you can do Await.result as described here.

And if you want to do it in a more monadic style (in a for-comprehension, like you tried), cats has an OptionT type that will help with that, and I think scalaz does as well. But whether you want to get into either of those libraries is up to you.

Upvotes: 1

Related Questions