Scala,Either[_, Seq[Either[_, T]] 到 Either[_, Seq[T]]

Scala, Either[_, Seq[Either[_, T]] to Either[_, Seq[T]]

下面是代码的排序:https://scastie.scala-lang.org/bQMGrAKgRoOFaK1lwCy04g

我有两个 JSON API 端点。首先,items.cgi,returns item对象列表,格式如下

$ curl http://example.com/items.cgi
[
    ...
    { sn: "KXB1333", ownerId: 3, borrowerId: 0 },
    { sn: "KCB1200", ownerId: 1, borrowerId: 2 },
    ...
]

borrowerId == 0表示项目没有借款人。

二、users.cgi、returns用户由id查询参数指定

$ curl http://example.com/user.cgi?id=1
{ id: 1, name: "frank" }

API可能很糟糕,但我必须处理它。现在在 Scala 中,我想使用这个漂亮的数据模型

case class User(id: Int, name: String)
case class Item(sn: String, owner: User, borrower: Option[User])

我还有以下用于执行 HTTP 请求的方法

case class ApiFail(reason: String)
def get[T](url: String): Either[ApiFail, T] = ??? /* omitted for brevity */

get() 函数使用一些魔法从 URL 中获取 JSON 并从中构造一个 T(它使用了一些库)。在 IO 故障或错误的 HTTP 状态下,它 returns Left.

我想写下面的函数

def getItems: Either[ApiFail, Seq[Item]]

它应该获取项目列表,对于每个项目获取链接的用户和 return 一个新的 Item 列表,否则在 any 上失败HTTP 请求失败。 (对于具有相同 ID 的用户可能会有冗余请求,但我不关心 memoization/caching。)

到目前为止我只写了这个函数

def getItems: Either[ApiFail, Seq[Either[ApiFail, Item]]]

其中检索某些用户的失败仅对相应的项目而不是整个结果是致命的。这是实现

def getItems: Either[ApiFail, Seq[Either[ApiFail, Item]]] = {
    case class ItemRaw(sn: String, ownerId: Int, borrowerId: Int)

    get[List[ItemRaw]]("items.cgi").flatMap(itemRawList => Right(
        itemRawList.map(itemRaw => {
            for {
                owner <- get[User](s"users.cgi?id=${itemRaw.ownerId}")
                borrower <-
                    if (itemRaw.borrowerId > 0)
                        get[User](s"users.cgi?id=${itemRaw.borrowerId}").map(Some(_))
                    else
                        Right(None)
            } yield
                Item(itemRaw.sn, owner, borrower)
        })
    ))
}

这似乎是一项家庭作业要求,但我经常想到我想从一个 wrapper 东西(m-monad?)切换到另一个,我是对于如何仅使用 包装函数 (c-combinators?)来做到这一点有点困惑。我当然可以切换到命令式实现。我只是好奇。

在 FP 世界中有一个词就是用来做这个的 - "Traverse" (link to cats implementation)。当你有一个 F[A] 和一个函数 A => G[B] 并且你想要一个 G[F[B]] 时使用它。这里,FListAItemRawGEither[ApiFail, _]BItem .当然,FG 是有一些限制的。

使用猫,你可以稍微改变你的方法:

import cats._, cats.implicits._

def getItems: Either[ApiFail, Seq[Item]] = {
  case class ItemRaw(sn: String, ownerId: Int, borrowerId: Int)

  get[List[ItemRaw]]("items.cgi").flatMap(itemRawList =>
    itemRawList.traverse[({type T[A]=Either[ApiFail, A]})#T, Item](itemRaw => {
      for {
        owner <- get[User](s"users.cgi?id=${itemRaw.ownerId}")
        borrower <-
          if (itemRaw.borrowerId > 0)
            get[User](s"users.cgi?id=${itemRaw.borrowerId}").map(Some(_))
          else
            Right(None)
      } yield
        Item(itemRaw.sn, owner, borrower)
    })
  )
}

话虽如此,我当然可以理解对完全走这条路犹豫不决。猫(和 scalaz)有很多东西可以吸收 - 虽然我建议你在某个时候这样做!

没有它们,您始终可以编写自己的实用方法来操作您常用的容器:

def seqEither2EitherSeq[A, B](s: Seq[Either[A, B]]): Either[A, Seq[B]] = {
  val xs: Seq[Either[A, Seq[B]]] = s.map(_.map(b => Seq(b)))
  xs.reduce{ (e1, e2) => for (x1 <- e1; x2 <- e2) yield x1 ++ x2 }
}

def flattenEither[A, B](e: Either[A, Either[A, B]]): Either[A, B] = e.flatMap(identity)

那么你想要的结果就是:

val result: Either[ApiFail, Seq[Item]] = flattenEither(getItems.map(seqEither2EitherSeq))