Scala,要么[_,Seq [要么[_,T]]要么[_,Seq [T]]

时间:2017-12-04 18:44:33

标签: scala monads either

以下代码的scaste:https://scastie.scala-lang.org/bQMGrAKgRoOFaK1lwCy04g

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

$ 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]]

它应该获取项目列表,为每个项目获取链接的用户并返回Item的新列表,或者在任何 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?)来完成它。我当然可以切换到命令式实现。我只是好奇。

1 个答案:

答案 0 :(得分:3)

有一个词可以在FP世界中做到这一点 - &#34; Traverse&#34; (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))