scala - 将 Eithers 的 HList 转换为 HList 的 Either
问题描述
我想定义一个接受 a 的函数,HList
它的元素是这样的,对于每个 element t
,都有一个T
这样的类型t: Either[String, T]
。我们将调用的函数validate
应该具有以下行为:
- 如果参数的所有元素都是
Right
,则返回Right
右投影映射参数的结果。 - 否则,返回 a ,其中列表包含参数中每个参数
Left[List[String]]
的左投影。Left
例子:
validate (Right (42) :: Right (3.14) :: Right (false) :: HNil)
>> Right (42 :: 3.14 :: false :: HNil)
validate (Right (42) :: Left ("qwerty") :: Left ("uiop") :: HNil)
>> Left (List ("qwerty", "uiop"))
一个示例用例:
case class Result (foo: Foo, bar: Bar, baz: Baz, qux: Qux)
def getFoo: Either[String, Foo] = ???
def getBar: Either[String, Bar] = ???
def getBaz: Either[String, Baz] = ???
def getQux: Either[String, Qux] = ???
def createResult: Either[String, Result] = {
validate (getFoo :: getBar :: getBaz :: getQux :: HNil) match {
case Right (foo :: bar :: baz :: qux :: HNil) => Right (Result (foo, bar, baz, qux))
case Left (errors) => Left ("The following errors occurred:\n" + errors.mkString ("\n"))
}
}
解决方案
我假设我们在整个答案中都有一些这样的测试数据:
scala> import shapeless.{::, HNil}
import shapeless.{$colon$colon, HNil}
scala> type In = Either[String, Int] :: Either[String, String] :: HNil
defined type alias In
scala> val good: In = Right(123) :: Right("abc") :: HNil
good: In = Right(123) :: Right(abc) :: HNil
scala> val bad: In = Left("error 1") :: Left("error 2") :: HNil
bad: In = Left(error 1) :: Left(error 2) :: HNil
使用自定义类型类
有很多方法可以做到这一点。我可能会使用一个自定义类型类来突出显示实例的归纳方式:
import shapeless.HList
trait Sequence[L <: HList] {
type E
type Out <: HList
def apply(l: L): Either[List[E], Out]
}
object Sequence {
type Aux[L <: HList, E0, Out0 <: HList] = Sequence[L] { type E = E0; type Out = Out0 }
implicit def hnilSequence[E0]: Aux[HNil, E0, HNil] = new Sequence[HNil] {
type E = E0
type Out = HNil
def apply(l: HNil): Either[List[E], HNil] = Right(l)
}
implicit def hconsSequence[H, T <: HList, E0](implicit
ts: Sequence[T] { type E = E0 }
): Aux[Either[E0, H] :: T, E0, H :: ts.Out] = new Sequence[Either[E0, H] :: T] {
type E = E0
type Out = H :: ts.Out
def apply(l: Either[E0, H] :: T): Either[List[E0], H :: ts.Out] =
(l.head, ts(l.tail)) match {
case (Right(h), Right(t)) => Right(h :: t)
case (Left(eh), Left(et)) => Left(eh :: et)
case (Left(eh), _) => Left(List(eh))
case (_, Left(et)) => Left(et)
}
}
}
然后你可以validate
这样写:
def validate[L <: HList](l: L)(implicit s: Sequence[L]): Either[List[s.E], s.Out] = s(l)
并像这样使用它:
scala> validate(good)
res0: scala.util.Either[List[String],Int :: String :: shapeless.HNil] = Right(123 :: abc :: HNil)
scala> validate(bad)
res1: scala.util.Either[List[String],Int :: String :: shapeless.HNil] = Left(List(error 1, error 2))
请注意,静态类型是正确的。
使用正确的折叠
你也可以通过折叠来更简洁地做到这一点Poly2
。
import shapeless.Poly2
object combine extends Poly2 {
implicit def eitherCase[H, T, E, OutT <: HList]:
Case.Aux[Either[E, H], Either[List[E], OutT], Either[List[E], H :: OutT]] = at {
case (Right(h), Right(t)) => Right(h :: t)
case (Left(eh), Left(et)) => Left(eh :: et)
case (Left(eh), _) => Left(List(eh))
case (_, Left(et)) => Left(et)
}
}
接着:
scala> good.foldRight(Right(HNil): Either[List[String], HNil])(combine)
res2: scala.util.Either[List[String],Int :: String :: shapeless.HNil] = Right(123 :: abc :: HNil)
scala> bad.foldRight(Right(HNil): Either[List[String], HNil])(combine)
res3: scala.util.Either[List[String],Int :: String :: shapeless.HNil] = Left(List(error 1, error 2))
我想这可能是“正确”的答案,假设你想单独坚持 Shapeless。该方法仅依赖于我个人不太喜欢Poly2
的隐式解析的一些奇怪细节(例如,我们不能定义combine
为 a )。val
使用小猫的序列
最后,您可以使用支持排序和遍历 hlist 的Kittens库:
scala> import cats.instances.all._, cats.sequence._
import cats.instances.all._
import cats.sequence._
scala> good.sequence
res4: scala.util.Either[String,Int :: String :: shapeless.HNil] = Right(123 :: abc :: HNil)
scala> bad.sequence
res5: scala.util.Either[String,Int :: String :: shapeless.HNil] = Left(error 1)
但请注意,这不会累积错误。
如果您想要最完整的 Typelevel 体验,我想您可以向 Kittens 添加一个操作,该操作将通过将它们映射到的实例来parSequence
为一个 hlist 累积错误(有关其工作原理的更多详细信息,请参阅我的博客文章)。不过,小猫目前不包括这个。Parallel
Validated
更新:并行测序
如果您愿意parSequence
,自己编写它实际上并不是一场噩梦:
import shapeless.HList, shapeless.poly.~>, shapeless.ops.hlist.{Comapped, NatTRel}
import cats.Parallel, cats.instances.all._, cats.sequence.Sequencer
def parSequence[L <: HList, M[_], P[_], PL <: HList, Out](l: L)(implicit
cmp: Comapped[L, M],
par: Parallel.Aux[M, P],
ntr: NatTRel[L, M, PL, P],
seq: Sequencer.Aux[PL, P, Out]
): M[Out] = {
val nt = new (M ~> P) {
def apply[A](a: M[A]): P[A] = par.parallel(a)
}
par.sequential(seq(ntr.map(nt, l)))
}
接着:
scala> parSequence(good)
res0: Either[String,Int :: String :: shapeless.HNil] = Right(123 :: abc :: HNil)
scala> parSequence(bad)
res1: Either[String,Int :: String :: shapeless.HNil] = Left(error 1error 2)
请注意,这确实会累积错误,但通过连接字符串。在列表中累积错误的 Cats 惯用方法如下所示:
scala> import cats.syntax.all._
import cats.syntax.all._
scala> val good = 123.rightNel[String] :: "abc".rightNel[String] :: HNil
good: Either[cats.data.NonEmptyList[String],Int] :: Either[cats.data.NonEmptyList[String],String] :: shapeless.HNil = Right(123) :: Right(abc) :: HNil
scala> val bad = "error 1".leftNel[String] :: "error 2".leftNel[Int] :: HNil
bad: Either[cats.data.NonEmptyList[String],String] :: Either[cats.data.NonEmptyList[String],Int] :: shapeless.HNil = Left(NonEmptyList(error 1)) :: Left(NonEmptyList(error 2)) :: HNil
scala> parSequence(good)
res3: Either[cats.data.NonEmptyList[String],Int :: String :: shapeless.HNil] = Right(123 :: abc :: HNil)
scala> parSequence(bad)
res4: Either[cats.data.NonEmptyList[String],String :: Int :: shapeless.HNil] = Left(NonEmptyList(error 1, error 2))
可能值得打开公关向小猫添加这样的东西。
推荐阅读
- javascript - 如何从 riot API 请求 rune png
- iframe - 编写 iframe 文档内容时再次注入 Javascript
- visual-studio - Visual Studio Extension 在停止调试进程之前捕获事件 (IVsDebugProcessNotify BeforeStopDebuggingProcess)
- reactjs - 错误:除非您的组件在 FirebaseAppProvider 中,否则无法调用 useFirebaseApp
- javascript - 为什么cheerio.load() 函数不能正常工作
- firebase - React Native Firebase - signInWithPhoneNumber 不返回确认结果
- javascript - 输入到文本区域
- android - Android 蓝牙 HCI:蓝牙控制器如何与 DSP 通信?
- powershell - Powershell 中的“Get-FileMetaData”不是 Cmdlet
- javascript - ReactJS 分页