Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

scala

scalaz

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?

like image 906
Blankman Avatar asked Feb 10 '17 00:02

Blankman


People also ask

Why should I wait for one API response to complete?

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.

Should you refactoring your API?

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.

How to deliver the complete object to the consumer of API?

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!”

Do you need to take a step back from Apis?

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.


Video Answer


1 Answers

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.

like image 60
Travis Brown Avatar answered Nov 16 '22 01:11

Travis Brown