使用 Arrow-kt 和 Kotlin 处理异步结果

问题描述

我有两个对外部系统的异步函数调用,返回任何一个,并且需要合并它们的结果。作为 Arrow-Kt 函数式编程的初学者,我想知道完成这项任务的最佳方式是什么。 以下是我目前正在使用的代码。它当然有效,但并不真正“感觉”是最直接的。我正在寻找一种更“实用”的风格来获得结果。 注意:成功的List结果的前期使用是必要的。

suspend fun getAs(): Either<Exception,List<A>> = Todo()
suspend fun getBs(): Either<Exception,List<B>> = Todo()
suspend fun doSomethingWithA(listA: List<A>): Unit = Todo()

launch {
    val deferredA = async { getAs() }
    val deferredB = async { getBs() }

    either<Exception,List<A>> {
        val listofAs = deferredA.await()
            .bimap(leftOperation = { e ->
                println("special message on error for A")
                e
            },rightOperation = { listA ->
                doSomethingWithA(listA)
                listA
            })
            .bind()
        val listofBs = deferredB.await().bind()

        listofAs.filter { it.someId !in listofBs.map { it.someProperty } }
    }
    .map { /* handle result */ }
    .handleError { /* handle error */ }

}

另一种选择是像这样使用 map{} 函数

launch {
    val deferredA = async { getAs() }
    val deferredB = async { getBs() }

    deferredA.await()
        .bimap(leftOperation = { e ->
            println("special message on error for A")
            e
        },rightOperation = { listA ->
            doSomethingWithA(listA)
            deferredB.await().map { listB ->
                listA.filter { a -> a.someId !in listB.map { it.someProperty } }
            }
        })
        .map { /* handle result */ }
        .handleError { /* handle error */ }
}

解决方法

最简单的方法是将 either { }parZip 结合使用。 either { } 允许您从 A 中提取 Either<E,A>,而 parZip 是并行运行 suspend 函数的实用函数。

suspend fun getAs(): Either<Exception,List<A>> = TODO()
suspend fun getBs(): Either<Exception,List<B>> = TODO()
suspend fun doSomethingWithA(listA: List<A>): Unit = TODO()

either {
  val list = parZip(
    {
       getAs()
         .mapLeft { e -> println("special message on error for A"); e }
         .bind()
    },{ getBs().bind() },{ aas,bbs ->
      aas.filter { a -> a.someId !in bbs.map { it.someProperty }
    }
  )

  /* Work with list and return value to `either { } */
}.handleError { /* handle error */ }

此处 bind()A 中提取 Either<E,A>。我们在 parZip 中执行此操作,以便每当遇到 Left 时,它都会使 either { } 块短路,并且这样做还会取消 parZip 中仍在运行的任务。

这样,如果 getAs() 立即与 Left 一起返回,则它成为 either { } 的输出值,getBs() 被取消。

,

我正要发布一个非常相似的答案。请注意,getAsgetBs 并不是真正连续的,因为 getBs 不需要执行 getAs 的结果。他们只是碰巧最终需要合并结果。换句话说:我们可以并行化

这里,除了 Simon 的建议之外,我还有一些额外的事情要做。 (在此示例中,我将用 NetworkUserDbUser 替换 A 和 B 以尝试为其赋予一些语义,否则过滤器上的那些“id”属性将不起作用。

捕获错误并将它们映射到每个有效函数上的强类型域错误。

这将有助于减轻程序其余部分的负担,并在此之上提供更安全的域错误层次结构,我们可以在需要时对其进行详尽的评估。

suspend fun <A> getUsersFromNetwork(): Either<DomainError,List<NetworkUser>> =
 Either.catch { fetchUsers() }
   .mapLeft { exception ->
     println("special message on error for A")
     exception.toDomain()
   }

让 doSomething 函数返回或者,以防它也失败。

这是一个你说它在初始 get 之后需要的函数,这意味着 flatMap 或 bind(它们是等效的)。如果我们将其提升到 Either 中,将确保错误短路按预期发生,因此此操作永远不会在最初未成功的情况下运行。

我建议这样做,因为我怀疑你在这里的这个操作也是你的代码中第一次操作的结果,可能是将第一次操作的结果存储在本地缓存中或其他类型的影响只是消耗该结果。

suspend fun doSomethingWithNetworkUsers(listA: List<NetworkUser>): Either<DomainError,Unit> = TODO()

所以我们将依赖的组合函数看起来像这样:

suspend fun getUsersFromNetwork(): Either<DomainError,List<NetworkUser>> = TODO()
suspend fun getUsersFromDb(): Either<DomainError,List<DbUser>> = TODO()
suspend fun doSomethingWithNetworkUsers(listA: List<NetworkUser>): Either<DomainError,Unit> = TODO()

和程序:

fun CoroutineScope.program() {
  launch {
    either {
      parZip(
        {
          val networkUsers = getUsersFromNetwork().bind()
          doSomethingWithNetworkUsers(networkUsers).bind()
          networkUsers
        },{ getUsersFromDb().bind() }
      ) { networkUsers,dbUsers ->
        networkUsers.filter { networkUser ->
          networkUser.id !in dbUsers.map { dbUser -> dbUser.id }
        }
      }
    }
    .map { /* do something with the overall result */ }
    .handleError { /* can recover from errors here */ }
    // Alternatively:
    // .fold(ifLeft = {},ifRight = {}) for handling both sides.
  }
}

通过将第一个操作作为先绑定的组合操作来执行,就像从上面的代码中提取的以下代码段一样,我们确保这两个操作都在 parZip lambda 组合结果发生之前完成。

val networkUsers = getUsersFromNetwork().bind()
doSomethingWithNetworkUsers(networkUsers).bind()
networkUsers