Blankman
Blankman

Reputation: 267140

My API is all returning Future[Option[T]], how to combine them nicely in a for-compr

All of my API methods return Future[Option[T]], trying to figure out how to elegantly perform the following:

case class UserProfile(user: User, location: Location, addresses: Address)

The below code currently doesn't compile because user, location, and address are all Option[User], Option[Location] and Option[Address]

val up = for {
 user <- userService.getById(userId)
 location <- locationService.getById(locationId)
 address <- addressService.getById(addressId)
} yield UserProfile(user, location, address)

I remember that scalaz has OptionT but I have never really used it before and not sure how to apply it to my situation.

If say user, location or address actually return None, what would happen when using OptionT when I need to apply it to 3 models in this case?

Upvotes: 14

Views: 627

Answers (1)

Travis Brown
Travis Brown

Reputation: 139048

Some simple definitions for the sake of a complete working example:

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

type User = String
type Location = String
type Address = String

case class UserProfile(user: User, location: Location, addresses: Address)

def getUserById(id: Long): Future[Option[User]] = id match {
  case 1 => Future.successful(Some("Foo McBar"))
  case _ => Future.successful(None)
}

def getLocationById(id: Long): Future[Option[Location]] = id match {
  case 1 => Future.successful(Some("The Moon"))
  case _ => Future.successful(None)
}

def getAddressById(id: Long): Future[Option[Address]] = id match {
  case 1 => Future.successful(Some("123 Moon St."))
  case _ => Future.successful(None)
}

And for the sake of completeness, here's what the Scalaz-free implementation would look like:

def getProfile(uid: Long, lid: Long, aid: Long): Future[Option[UserProfile]] =
  for {
    maybeUser     <- getUserById(uid)
    maybeLocation <- getLocationById(lid)
    maybeAddress  <- getAddressById(aid)
  } yield (
    for {
      user     <- maybeUser
      location <- maybeLocation
      address  <- maybeAddress
    } yield UserProfile(user, location, address)
  )

I.e. we have to nest for-comprehensions, just like we'd have to nest map to transform e.g. the Int value that might be inside a Future[Option[Int]].

The OptionT monad transformer in Scalaz or Cats is designed to allow you to work with types like Future[Option[A]] without this nesting. For example you could write this:

import scalaz.OptionT, scalaz.std.scalaFuture._

def getProfile(uid: Long, lid: Long, aid: Long): OptionT[Future, UserProfile] =
  for {
    user     <- OptionT(getUserById(uid))
    location <- OptionT(getLocationById(lid))
    address  <- OptionT(getAddressById(aid))
  } yield UserProfile(user, location, address)

Or if you wanted a Future[Option[UserProfile]] you can just call run:

def getProfile(uid: Long, lid: Long, aid: Long): Future[Option[UserProfile]] = (
  for {
    user     <- OptionT(getUserById(uid))
    location <- OptionT(getLocationById(lid))
    address  <- OptionT(getAddressById(aid))
  } yield UserProfile(user, location, address)
).run

And then:

scala> getProfile(1L, 1L, 1L).foreach(println)
Some(UserProfile(Foo McBar,The Moon,123 Moon St.))

If any of the intermediate results are None, the whole thing will be None:

scala> getProfile(1L, 1L, 0L).foreach(println)
None

scala> getProfile(0L, 0L, 0L).foreach(println)
None

And of course if any of the requests fail, the whole thing fails with the first error.

As a footnote, if the requests don't depend on each other, you can compose them applicatively instead of monadically:

import scalaz.Scalaz._

def getProfile(uid: Long, lid: Long, aid: Long): Future[Option[UserProfile]] = (
  OptionT(getUserById(uid)) |@|
  OptionT(getLocationById(lid)) |@|
  OptionT(getAddressById(aid))
)(UserProfile.apply _).run

This models the computation more accurately and may be more efficient since it can run the requests in parallel.

Upvotes: 20

Related Questions