6

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.

1
  • Have you looked at Clump or Fetch? Commented Jan 9, 2018 at 9:30

4 Answers 4

18

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.

Sign up to request clarification or add additional context in comments.

3 Comments

Can't remember about Cats, but in Scalaz there's a handy ApplicativeBuilder instance available for applicative functors which is constructed via |@| symbols and it takes a function to be applied as an argument. It's a generalized version of map2 for any number of parameters. Code ends up looking something like this: (getName |@| getAge |@| getEmail)((name, age, email) => Person(name, age, email))
thank you Bartosz for the theoretical explanation. that makes me understand things better.
@Bartosz Milewski, thank you for your answer, it took me a day to figure out the idea, but I understand it now, it is really beautiful
7

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)

12 Comments

Sure thing, one other thing I thought I'd mention, is that EitherNel[String, Option[A]] looks like a bit of an odd type, because why have an Option inside an Either. Now you have two ways to specify errors and it makes everything more complicated. Why isn't the None case handled by the surrounding Either? :)
yes you totally right. this morning i was thinking exactly thr same. better without Option
FWIW, I'll edit the answer to provide a way without Option :)
im trying this code and it tells me that parMapN is not a member of FutureResult[String] in the documentation i see example with monads (Either) but not with monads transformers. am i missing anything?
cool i will investigate :) thanks a lot for all the examples and the explanations
|
0

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)

Comments

0

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)

6 Comments

this is a very simple and clean solution but i see some things i do not like:
I don't think this answers the question, as you don't get error accumulation at all.
True. The for comprehension just fails on the first failure.
By the defintion of def getPerson(id: Int): Future[Option[Person]] in question there is no need for error accumulation, or am I missing something? The only requirement is to run the futures in parallel. Which is perfectly pragmatic way how @James Ward proposed.
@James due to the sequential binding of the Futures in the for-comprehension there is not fail-fast (i.e. futureName takes 10 seconds to return a Future Successful whereas futureAge and futureEmail takes 1 to fail, that means that waiting for the final result or person (future failed ) would take 10 seconds). i wonder if failfast can be achieved with another approach like Lukas. what i dont like about this approach is that errors and error handling is not a part of the method signature or the result type and "recover" is not mandatory plus uniform exception handling for different methods
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.