简体   繁体   English

将 Ei 的 HList 转换为 HList 的任一个

[英]Transform a HList of Eithers to an Either of a HList

I want to define a function that accepts a HList whose elements are such that, for each element t , there is a type T such that t: Either[String, T] .我想定义一个函数,它接受一个HList它的元素是这样的,对于每个元素t ,都有一个类型T使得t: Either[String, T] The function, which we will call validate , should have the following behaviour:我们将调用validate的函数应该具有以下行为:

  • If all elements of the parameter are Right , return Right of the result of mapping the parameter with right-projection.如果参数的所有元素都是Right ,则返回Right -projection 映射参数的结果的Right
  • Otherwise, return a Left[List[String]] , where the list contains the left-projection for each Left in the parameter.否则,返回一个Left[List[String]] ,其中列表包含参数中每个Left的左投影。

Examples:例子:

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

An example use case:一个示例用例:

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

I'll assume we have some test data like this throughout this answer:我假设我们在整个答案中有一些这样的测试数据:

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

Using a custom type class使用自定义类型类

There are many ways you could do this.有很多方法可以做到这一点。 I'd probably use a custom type class that highlights the way instances are built up inductively:我可能会使用自定义类型类来突出显示实例的归纳构建方式:

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

Then you can write validate like this:然后你可以像这样编写validate

def validate[L <: HList](l: L)(implicit s: Sequence[L]): Either[List[s.E], s.Out] = s(l)

And use it like this:并像这样使用它:

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

Note that the static types come out right.请注意,静态类型是正确的。

Using a right fold使用正确的折叠

You could also do it a little more concisely by folding with a Poly2 .您也可以通过使用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) 
    }
}

And then:进而:

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

I guess this is probably the "right" answer, assuming you want to stick to Shapeless alone.我想这可能是“正确”的答案,假设您想单独坚持使用 Shapeless。 The Poly2 approach just relies on some weird details of implicit resolution (we couldn't define combine as a val , for example) that I personally don't really like. Poly2方法仅依赖于我个人并不真正喜欢的隐式解析的一些奇怪细节(例如,我们无法将combine定义为val )。

Using Kittens's sequence使用小猫的序列

Lastly you could use the Kittens library, which supports sequencing and traversing hlists:最后,您可以使用支持排序和遍历 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)

Note that this doesn't accumulate errors, though.请注意,这不会累积错误。

If you wanted the most complete possible Typelevel experience I guess you could add a parSequence operation to Kittens that would accumulate errors for an hlist of eithers via the Parallel instance mapping them to Validated (see my blog post here for more detail about how this works).如果您想要最完整的 Typelevel 体验,我想您可以向 Kittens 添加一个parSequence操作,该操作会通过将它们映射到ValidatedParallel实例来累积任何一个 hlist 的错误(有关其工作原理的更多详细信息,请参阅我的博客文章) . Kittens doesn't currently include this, though.不过,小猫目前不包括这个。

Update: parallel sequencing更新:平行测序

If you want parSequence , it's not actually that much of a nightmare to write it yourself:如果你想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)))
}

And then:进而:

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)

Note that this does accumulate errors, but by concatenating the strings.请注意,这确实会累积错误,但通过连接字符串。 The Cats-idiomatic way to accumulate errors in a list would look like this:在列表中累积错误的 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))

It'd probably be worth opening a PR to add something like this to Kittens.可能值得打开一个 PR 来为 Kittens 添加这样的东西。

I managed to arrive at a solution essentially identical to Travis Brown's right-fold solution, with a few additions:我设法得出了一个与 Travis Brown 的右折叠解决方案基本相同的解决方案,但有一些补充:

class Validate[E] {
    def apply[L <: HList] (hlist: L) (implicit folder: RightFolder[L, Either[List[E], HNil], combine.type]) =
        hlist.foldRight (Right (HNil) : Either[List[E], HNil]) (combine)
}

object combine extends Poly2 {
    implicit def combine[E, H, T <: HList]
    : ProductCase.Aux[Either[E, H] :: Either[List[E], T] :: HNil, Either[List[E], H :: T]] = use {
        (elem: Either[E, H], result: Either[List[E], T]) => (elem, result) match {
            case (Left (error), Left (errors)) => Left (error :: errors)
            case (Left (error), Right (_)) => Left (error :: Nil)
            case (Right (_), Left (errors)) => Left (errors)
            case (Right (value), Right (values)) => Right (value :: values)
        }
    }
}

def validate[E] = new Validate[E]

This allows the left type to vary, and allows the syntax:这允许左类型变化,并允许语法:

validate[String] (getFoo :: getBar :: getBaz :: getQux :: HNil) match {
    case Right (foo :: bar :: baz :: qux :: HNil) => ???
    case Left (errors) => ???
}

Admittedly this is the first time that I have used Poly .诚然,这是我第一次使用Poly It blew my mind to see that this actually worked.看到这确实有效,我大吃一惊。 Infuriatingly, the static analysis provided by my IDE (IntelliJ) is not clever enough to infer the types of the terms in the match cases.令人气愤的是,我的 IDE (IntelliJ) 提供的静态分析不够聪明,无法推断匹配案例中的术语类型。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM