将一个HList转换为一个HList

时间:2019-12-09 05:11:51

标签: scala shapeless

我想定义一个接受HList的函数,该函数的元素是这样的,对于每个元素t,都有一个类型T这样的t: Either[String, T]。该函数(我们将称为validate)应具有以下行为:

  • 如果参数的所有元素均为Right,则返回参数映射为右投影的结果的Right
  • 否则,返回一个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"))
    }
}

2 个答案:

答案 0 :(得分:4)

我假设我们在整个答案中都有这样的测试数据:

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定义为val)。

使用小猫的顺序

最后,您可以使用Kittens库,该库支持对hlist进行排序和遍历:

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操作,该操作会通过Parallel实例将它们映射到{{1} }(有关其工作原理的更多信息,请参见my blog post here)。小猫目前还不包括这个。

更新:并行测序

如果您想要Validated,那么自己写它实际上并不是梦em,

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

请注意,这确实会累积错误,但是会串联字符串。 Cats惯用的在列表中累积错误的方法如下所示:

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)

打开PR为小猫添加类似内容可能是值得的。

答案 1 :(得分:0)

我设法得到了一个与特拉维斯·布朗的右对折解决方案基本相同的解决方案,并增加了一些内容:

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]

这允许左类型改变,并允许语法:

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

诚然,这是我第一次使用Poly。看到这确实有效,我大吃一惊。令人愤怒的是,我的IDE(IntelliJ)提供的静态分析还不足以推断出匹配情况下的术语类型。