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]]
时使用它。这里,F
是List
,A
是ItemRaw
,G
是Either[ApiFail, _]
,B
是Item
.当然,F
和 G
是有一些限制的。
使用猫,你可以稍微改变你的方法:
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))
下面是代码的排序: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]]
时使用它。这里,F
是List
,A
是ItemRaw
,G
是Either[ApiFail, _]
,B
是Item
.当然,F
和 G
是有一些限制的。
使用猫,你可以稍微改变你的方法:
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))