【问题标题】:Scala, Either[_, Seq[Either[_, T]] to Either[_, Seq[T]]Scala, Either[_, Seq[Either[_, T]] 到 Either[_, Seq[T]]
【发布时间】:2017-12-04 18:44:33
【问题描述】:

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

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

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

borrowerId == 0 表示项目没有借阅者。

第二,users.cgi,返回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 状态错误时,它会返回 Left

我想写如下函数

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

它应该获取项目列表,为每个项目获取链接的用户并返回Items 的新列表,或者在任何 HTTP 请求失败时失败。 (对于具有相同 ID 的用户可能会有多余的请求,但我还不关心 memoization/缓存。)

目前我只写了这个函数

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)
        })
    ))
}

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

【问题讨论】:

    标签: scala monads either


    【解决方案1】:

    在 FP 世界中有一个词可以准确地做到这一点 - “遍历”(link to cats implementation)。当你有一个F[A] 和一个函数A =&gt; 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))
    

    【讨论】:

    • 感谢您的详细解释。有很多东西要学。 traverse 的类型参数有点令人费解。我在来源中看到它需要 3,但您只指定了两个。它可以在没有带有-Ypartial-unification scalac 标志的类型参数的情况下工作。 scastie.scala-lang.org/8c1pdg7eTeqzQGRUDLPQOw
    • cats 使用 simulacrum (github.com/mpilquist/simulacrum) 为类型类提供一些额外的语法。所以我调用的遍历方法实际上是在一些生成的Traverse.Ops 类或类似的类中,从List 隐式转换。 FA 类型参数是从我调用它的对象 (itemRawList) 中获得的,因此只需要 GB 类型参数。
    猜你喜欢
    • 2019-09-23
    • 2011-11-06
    • 1970-01-01
    • 2020-08-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-10
    相关资源
    最近更新 更多