【问题标题】:Making multiple API calls in a functional way以函数式方式进行多个 API 调用
【发布时间】:2018-01-07 14:16:23
【问题描述】:

使用 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 monad 用于在 API 调用期间返回可能的错误,Option 用于在 API 未找到资源时返回 None(这种情况不是错误,而是可能的和期望的结果类型)。

以函数方式实现的方法

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

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

此方法应使用上面定义的三个 API 调用方法异步组合并返回 Person 或 None 如果任何 API 调用失败或任何 API 调用返回 None(整个 Person 实体无法组合)

要求

出于性能原因,所有 API 调用都必须以并行方式完成

我的猜测

我认为最好的选择是使用 Cats Semigroupal Validated,但是在尝试处理 Future 和这么多嵌套的 Monad 时我迷路了:S

谁能告诉我你将如何实现这个(即使改变方法签名或主要概念)或指向正确的资源?我对 Cats 和 Algebra 在编码方面很陌生,但我想学习如何处理这种情况,以便我可以在工作中使用它。

【问题讨论】:

标签: scala functional-programming category-theory scala-cats


【解决方案1】:

这里的关键要求是必须并行完成。这意味着使用 monad 的明显解决方案已经出局,因为 monadic bind 是阻塞的(它需要结果以防它必须在其上分支)。所以最好的选择是使用applicative。

我不是 Scala 程序员,所以我无法向您展示代码,但其想法是应用函子可以提升多个参数的函数(常规函子使用 map 提升单个参数的函数)。在这里,您将使用类似map3 的东西来提升Person 的三参数构造函数以处理三个FutureResults。搜索“在 Scala 中应用的未来”会返回一些命中。 EitherOption 也有应用程序实例,并且与 monad 不同,应用程序可以轻松组合在一起。希望这会有所帮助。

【讨论】:

  • 不记得 Cats,但在 Scalaz 中有一个方便的 ApplicativeBuilder 实例可用于应用函子,它是通过 |@| 符号构造的,它需要一个函数作为参数应用。对于任意数量的参数,它是map2 的通用版本。代码最终看起来像这样:(getName |@| getAge |@| getEmail)((name, age, email) => Person(name, age, email))
  • 感谢 Bartosz 的理论解释。这让我更好地理解事情。
  • @Bartosz Milewski,谢谢你的回答,我花了一天的时间才弄明白这个想法,但现在我明白了,真的很漂亮
【解决方案2】:

您可以使用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 documentation

编辑:这是没有内部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。现在你有两种方法来指定错误,它使一切变得更加复杂。为什么None 的情况不被周围的Either 处理? :)
  • 是的,你完全正确。今天早上我的想法完全一样。没有选项更好
  • FWIW,我将编辑答案以提供一种没有选项的方法:)
  • 我正在尝试此代码,它告诉我 parMapN 不是文档中 FutureResult[String] 的成员,我看到了带有 monads 的示例(或者),但没有使用 monads 转换器。我错过了什么吗?
  • 酷我会调查 :) 非常感谢所有的例子和解释
【解决方案3】:

这是我遇到的唯一解决方案,但仍然不满意,因为我觉得它可以以更清洁的方式完成

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)

【讨论】:

    【解决方案4】:

    就我个人而言,我更喜欢将所有不成功的条件都归结为 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 => ???
    }
    

    请注意,这不会并行运行请求,为此您需要将未来的创建移到 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)
    

    【讨论】:

    • 这是一个非常简单和干净的解决方案,但我看到一些我不喜欢的东西:
    • 我认为这不能回答问题,因为您根本不会积累错误。
    • 是的。 for 理解在第一次失败时就失败了。
    • 根据 def getPerson(id: Int): Future[Option[Person]] 的定义,有问题不需要累积错误,还是我遗漏了什么?唯一的要求是并行运行期货。这是@James Ward 提出的非常务实的方式。
    • @James 由于 Futures 在理解中的顺序绑定没有快速失败(即 futureName 需要 10 秒才能返回 Future Success 而 futureAge 和 futureEmail 需要 1 失败,即意味着等待最终结果或人(未来失败)将需要 10 秒)。我想知道是否可以使用 Lukas 之类的另一种方法来实现故障恢复。我不喜欢这种方法的是错误和错误处理不是方法签名或结果类型的一部分,并且“恢复”不是强制性的,加上不同方法的统一异常处理
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-11-11
    • 2022-06-18
    • 2020-03-22
    • 1970-01-01
    • 2018-03-21
    • 1970-01-01
    • 2020-12-06
    相关资源
    最近更新 更多