Enrique Molina
Enrique Molina

Reputation: 85

Making multiple API calls in a functional way

What would it be the best approach to solve this problem in the most functional (algebraic) way by using Scala and Cats (or maybe another library focused on Category Theory and/or functional programming)?

Resources

Provided we have the following methods which perform REST API calls to retrieve single pieces of information?

type FutureApiCallResult[A] = Future[Either[String, Option[A]]]

def getNameApiCall(id: Int): FutureApiCallResult[String]
def getAgeApiCall(id: Int): FutureApiCallResult[Int]
def getEmailApiCall(id: Int): FutureApiCallResult[String]

As you can see they produce asynchronous results. The Either monad is used to return possible errors during API calls and Option is used to return None whenever the resource is not found by the API (this case is not an error but a possible and desired result type).

Method to implement in a functional way

case class Person(name: String, age: Int, email: String)

def getPerson(id: Int): Future[Option[Person]] = ???

This method should used the three API calls methods defined above to asynchronously compose and return a Person or None if either any of the API calls failed or any of the API calls return None (the whole Person entity cannot be composed)

Requirements

For performance reasons all the API calls must be done in a parallel way

My guess

I think the best option would be to use the Cats Semigroupal Validated but I get lost when trying to deal with Future and so many nested Monads :S

Can anyone tell me how would you implement this (even if changing method signature or main concept) or point me to the right resources? Im quite new to Cats and Algebra in coding but I would like to learn how to handle this kind of situations so that I can use it at work.

Upvotes: 6

Views: 2642

Answers (4)

James Ward
James Ward

Reputation: 29433

Personally I prefer to collapse all non-success conditions into the Future's failure. That really simplifies the error handling, like:

val futurePerson = for {
  name  <- getNameApiCall(id)
  age   <- getAgeApiCall(id)
  email <- getEmailApiCall(id)
} yield Person(name, age, email)

futurePerson.recover {
  case e: SomeKindOfError => ???
  case e: AnotherKindOfError => ???
}

Note that this won't run the requests in parallel, to do so you'd need to move the future's creation outside of the for comprehension, like:

val futureName = getNameApiCall(id)
val futureAge  = getAgeApiCall(id)
val futureEmail = getEmailApiCall(id)

val futurePerson = for {
  name  <- futureName
  age   <- futureAge
  email <- futureEmail
} yield Person(name, age, email)

Upvotes: 0

Luka Jacobowitz
Luka Jacobowitz

Reputation: 23532

You can make use of the cats.Parallel type class. This enables some really neat combinators with EitherT which when run in parallel will accumulate errors. So the easiest and most concise solution would be this:

type FutureResult[A] = EitherT[Future, NonEmptyList[String], Option[A]]

def getPerson(id: Int): FutureResult[Person] = 
  (getNameApiCall(id), getAgeApiCall(id), getEmailApiCall(id))
    .parMapN((name, age, email) => (name, age, email).mapN(Person))

For more information on Parallel visit the cats documentation.

Edit: Here's another way without the inner Option:

type FutureResult[A] = EitherT[Future, NonEmptyList[String], A]

def getPerson(id: Int): FutureResult[Person] = 
  (getNameApiCall(id), getAgeApiCall(id), getEmailApiCall(id))
    .parMapN(Person)

Upvotes: 7

Bartosz Milewski
Bartosz Milewski

Reputation: 11650

The key requirement here is that it has to be done in parallel. It means that the obvious solution using a monad is out, because monadic bind is blocking (it needs the result in case it has to branch on it). So the best option is to use applicative.

I'm not a Scala programmer, so I can't show you the code, but the idea is that an applicative functor can lift functions of multiple arguments (a regular functor lifts functions of single argument using map). Here, you would use something like map3 to lift the three-argument constructor of Person to work on three FutureResults. A search for "applicative future in Scala" returns a few hits. There are also applicative instances for Either and Option and, unlike monads, applicatives can be composed together easily. Hope this helps.

Upvotes: 18

Enrique Molina
Enrique Molina

Reputation: 85

this is the only solution i came across with but still not satisfied because i have the feeling it could be done in a cleaner way

import cats.data.NonEmptyList
import cats.implicits._

import scala.concurrent.Future

case class Person(name: String, age: Int, email: String)

type FutureResult[A] = Future[Either[NonEmptyList[String], Option[A]]]

def getNameApiCall(id: Int): FutureResult[String] = ???
def getAgeApiCall(id: Int): FutureResult[Int] = ???
def getEmailApiCall(id: Int): FutureResult[String] = ???

def getPerson(id: Int): FutureResult[Person] =
(
  getNameApiCall(id).map(_.toValidated),
  getAgeApiCall(id).map(_.toValidated),
  getEmailApiCall(id).map(_.toValidated)
).tupled // combine three futures
  .map {
    case (nameV, ageV, emailV) =>
      (nameV, ageV, emailV).tupled // combine three Validated
        .map(_.tupled) // combine three Options
        .map(_.map { case (name, age, email) => Person(name, age, email) })   // wrap final result
  }.map(_.toEither)

Upvotes: 0

Related Questions