以函数式的方式进行多个API调用

6

使用Scala和Cats(或其他专注于范畴论和/或函数式编程的库)最适合以最功能性(代数)的方式解决此问题的方法是什么?

资源

假设我们有以下方法,它们执行REST API调用以检索单个信息片段?

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]

正如您所看到的,它们产生了异步结果。Either单子用于在API调用期间返回可能的错误,而Option用于在API未找到资源时返回None(此情况不是错误,而是可能的和期望的结果类型)。

以函数式方式实现的方法

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

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

这种方法应该使用上面定义的三个API调用方法来异步地组合并返回一个人物对象,如果其中任何一个API调用失败或任何一个API调用返回None(整个人物实体无法被组合),则返回None。
要求为了性能原因,所有API调用必须以并行方式完成。
我猜最好的选择是使用Cats Semigroupal Validated,但当尝试处理Future和许多嵌套的Monads时,我就迷失了:S
是否有人可以告诉我如何实现这个(即使更改方法签名或主要概念)或指向正确的资源? 我对Cats和编码中的代数学相当新,但我想学习如何处理这种情况,以便在工作中使用它们。

你看过Clump或者Fetch吗? - mitchus
4个回答

19
关键要求是必须并行处理。这意味着明显使用单子的解决方案不可行,因为单子绑定是阻塞的(如果必须在其上进行分支,则需要结果)。所以最好的选择是使用应用函子。
我不是Scala程序员,因此无法展示代码,但是想法是应用函子可以提升多参数函数(常规函子使用map提升单参数函数)。在这里,您将使用类似于map3的东西,将Person的三个参数构造器提升为三个FutureResult的工作。搜索“Scala中的应用程序未来”会返回一些结果。还有EitherOption的应用实例,并且与单子不同,可以轻松地组合应用。希望这能有所帮助。

2
不记得猫的事情,但在Scalaz中有一个方便的ApplicativeBuilder实例可用于可应用函子,它是通过|@|符号构造的,并且它需要一个要作为参数应用的函数。它是任意数量参数的map2的广义版本。代码最终看起来像这样: (getName |@| getAge |@| getEmail)((name, age, email) => Person(name, age, email)) - slouc
感谢Bartosz提供的理论解释,这让我更好地理解了事情。 - Enrique Molina
@Bartosz Milewski,感谢您的回答,我花了一天时间来理解这个想法,但现在我明白了,它真的很美。 - neshkeev

8
您可以利用cats.Parallel类型类。这使得与EitherT一起使用时会积累错误的一些非常棒的组合器。因此,最简单和最简洁的解决方案是:
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))

欲了解更多关于Parallel的信息,请访问cats文档

编辑:以下是不使用内部Option的另一种方法:

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

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

当然,我想提一件事,EitherNel [String,Option [A]] 看起来有点奇怪,因为为什么要在 Either 中放置一个 Option。现在你有两种方法来指定错误,这使得一切变得更加复杂。为什么不让周围的 Either 处理 None 情况呢? :) - Luka Jacobowitz
是的,你完全正确。今天早上我也在思考同样的问题。不使用Option会更好。 - Enrique Molina
顺便说一句,我会编辑答案,提供一种不使用Option的方法 :) - Luka Jacobowitz
抱歉,我的错。现在工作了!太棒了! 卢卡斯,最后一个问题,你如何快速失败?(我们显然可以省略错误积累) - Enrique Molina
1
很酷,我会调查 :) 非常感谢您提供的所有示例和解释。 - Enrique Molina
显示剩余7条评论

0

这是我找到的唯一解决方案,但仍然不满意,因为我感觉它可以用更简洁的方式完成。

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

个人而言,我更喜欢将所有非成功条件都折叠到 Future 的失败中。这样可以真正简化错误处理,例如:

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

请注意,这不会并行运行请求,要实现并行运行,您需要将future的创建移动到for推导之外,例如:
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)

这是一个非常简单和干净的解决方案,但我看到了一些我不喜欢的东西: - Enrique Molina
我认为这并没有回答问题,因为你根本不会出现错误累积。 - Luka Jacobowitz
正确。for 推导式只在第一个失败时停止。 - James Ward
1
根据 def getPerson(id: Int): Future[Option[Person]] 的定义,是否不需要错误累积呢?或者我漏掉了什么?唯一的要求是并行运行 futures。这正是 @James Ward 建议的完美实用方式。 - Teliatko
@James 由于for-comprehension中Futures的顺序绑定,所以没有快速失败(即futureName需要10秒才能返回一个Future Successful,而futureAge和futureEmail需要1秒钟就会失败,这意味着等待最终结果或人(future failed)需要10秒钟)。我想知道是否可以通过另一种方法实现failfast,比如Lukas。我不喜欢这种方法的原因是错误和错误处理不是方法签名或结果类型的一部分,“recover”也不是强制性的,而且不同方法的异常处理不统一。 - Enrique Molina
确实,这会导致顺序失败。我认为可以通过创建自定义的 Promise 来解决这个问题。 - James Ward

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接