Making multiple api calls in a functional way

What would be the best approach to solve this problem in the most functional (algebraic) way using Scala and Cats (or, possibly, another library focused on category theory and / or functional programming)?

Resources

If we have the following methods that make REST API calls to extract individual 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. Each monad is used to return possible errors during API calls, and the option is used to return None when the resource is not found by the API (this case is not an error, but the desired type of result is possible).

Functional implementation method

 case class Person(name: String, age: Int, email: String) def getPerson(id: Int): Future[Option[Person]] = ??? 

This method should use the three methods of API calls defined above to asynchronously link and return Person or None if any of the API calls fail or any of the API calls returns None (the entire Person object cannot be composed)

Requirements

For performance reasons, all API calls must be made in parallel.

My suggestion

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

Can someone tell me how you would implement this (even if you change the method signature or the main concept) or point me to the necessary resources? Im brand new for coding cats and algebras, but I would like to learn how to handle such situations so that I can use it at work.

+5
source share
4 answers

You can use a class like cats.Parallel . This allows some really neat combinators with EitherT that, when run in parallel, will accumulate errors. Thus, the simplest and most concise solution would be the following:

 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 about Parallel visit the cat documentation .

Edit : here is another way without an internal Option :

 type FutureResult[A] = EitherT[Future, NonEmptyList[String], A] def getPerson(id: Int): FutureResult[Person] = (getNameApiCall(id), getAgeApiCall(id), getEmailApiCall(id)) .parMapN(Person) 
+6
source

The key requirement here is that it must be executed in parallel. This means that there is no obvious solution using the monad, because monadic binding is blocked (it needs a result if it needs to fork on it). Therefore, 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 raise functions from several arguments (a regular functor lifts functions of a single argument using map ). Here you should use something like map3 to raise a constructor with three Person arguments to work on three FutureResult s. A search for "an application future in Scala" returns several hits. There are also applicative instances for Either and Option , and, unlike monads, applicators can be easily composed. Hope this helps.

+16
source

This is the only solution I came across, but still not satisfied, because I have a feeling that it can be done cleaner.

 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) 
0
source

Personally, I prefer to minimize all unforeseen conditions in the future. It really simplifies error handling, for example:

 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 => ??? } 

Please note that this will not run queries in parallel, for this you will need to move the future creation out of understanding, for example:

 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) 
0
source

Source: https://habr.com/ru/post/1274553/


All Articles