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

1

14

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?

Logarithm answered 10/2, 2017 at 0:17 Comment(1)
Since this is explicitly about OptionT, maybe it should have a scalaz tag (and possibly also monad-transformers and/or scala-cats)?Elvinelvina
E
20

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.

Elvinelvina answered 10/2, 2017 at 1:7 Comment(9)
That is what I call a home run man, thanks! |@| is like Future.sequence?Logarithm
@Logarithm Yep—the big difference is that it works with different types (maintaining both the types and arity).Elvinelvina
FYI: Future.onSuccess is deprecated since Scala 2.12 github.com/viktorklang/blog/blob/master/…Steamer
@TravisBrown I actually need to add another call, but it returns a Future[Seq[T]]. Is it possible to add this in now given the above?Logarithm
@n4to4 Updated to use foreach.Elvinelvina
@Logarithm What semantics do you want? Does an empty sequence work like a None? I'd guess what you want is essentially to first transform that into a Future[Option[Seq[T]]], but it probably merits a follow-up question with more details about the intent.Elvinelvina
An empty sequeuence is just fine i.e. None will not cause the entire thing to return None.Logarithm
I updated my question with the other property, which is a Future[Seq[Sale]]. This can be 0 or more results and 0 is fine.Logarithm
@Logarithm You can lift a fss: Future[Seq[Sale]] into an OptionT[Future, Seq[Sale]] with fss.liftM[OptionT].Elvinelvina

© 2022 - 2024 — McMap. All rights reserved.