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?
It also means that you need to wait for one API responses to complete before the next can begin, which is a big inefficient. Let’s look at a better way. With the Promise.all () method, we can pass in an array of promises. When all of them have resolved (or one fails), it will run our callback functions.
And, if your API ever needs to be consumed by the outside world, this may result in lots of painful refactoring to clean up. It’s important to “take a step back” every now and then.
However, delivering the complete object to the consumer of an API is very straightforward as one could simply do the following: Whereas sending strictly the requested user properties would look like this: It’s even trivial to justify this decision. “Heck, we already have the data, someone might need it, let’s just pass it along!”
And, if your API ever needs to be consumed by the outside world, this may result in lots of painful refactoring to clean up. It’s important to “take a step back” every now and then. Stop looking at APIs using the organizations de facto tools.
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.
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