简体   繁体   中英

How to define an HList type, but based on another HList type

Suppose I have a case class:

case class Foo(num: Int, str: String, bool: Boolean)

Now I also have a simple wrapper:

sealed trait Wrapper[T]
case class Wrapped[T](value: T) extends Wrapper[T]

(and some other implementations of Wrapper that aren't important here)

I can use Generic[Foo] to get an Aux that represents this case class: val genFoo = Generic[Foo] (in my real code I use LabelledGeneric so as not to lose the field names)

This provides me with a type representing the definition of an HList:

Generic.Aux[Foo, Int :: String :: Boolean :: HNil]

(when used with LabelledGeneric the definition is much more complex, but the same in essence)

Now I want to create a type definition for an HList that, instead of the raw types, includes the wrapped types instead. Example:

type WrappedHlist = Wrapper[Int] :: Wrapper[String] :: Wrapper[Boolean] :: HNil

I can then use this type definition to, for example, generate a Circe encoder/decoder (I've provided the necessary Encoder/Decoder for the Wrapper type).

All the necessary information exists at compile time, as the definition of the second HList is easily determined from the first. Right now, I can achieve this by writing out the definition manually, or by repeating the definition of the case class Foo with a redundant version where everything is defined as Wrappers. How can I create a definition that doesn't require me to repeat everything?

Well, this is doable with a little bit of type classes, path-dependent types and Aux pattern:

trait WrapperHelper[In] {
  type Out
  def wrap(i: In): Out
}
object WrapperHelper {
  type Aux[I, O] = WrapperHelper[I] { type Out = O }

  implicit val nilWH: WrapperHelper.Aux[HNil, HNil] = new WrapperHelper[HNil] {
    type Out = HNil
    def wrap(i: HNil): HNil = i
  }

  implicit def hconsWH[H, TI <: HList, TO <: HList](
    implicit
    tailWH: WrapperHelper.Aux[TI, TO]
  ): WrapperHelper.Aux[H :: TI, Wrapper[H] :: TO] = new WrapperHelper[H :: TI] {
    type Out = Wrapper[H] :: TO
    def wrap(i: H :: TI): Wrapper[H] :: TO = i match {
      case head :: tail => Wrapped(head) :: tailWH.wrap(tail)
    }
  }
}

def wrap[I](i: I)(implicit wh: WrapperHelper[I]): wh.Out = wh.wrap(i)

HNIl is treated as a special case where In = Out. For everything else... you still have to map the type recursively. While not pretty this should do what you want:

@ wrap(Generic[Foo].to(Foo(1, "", true)))
res7: Wrapper[Int] :: Wrapper[String] :: Wrapper[Boolean] :: HNil = Wrapped(1) :: Wrapped("") :: Wrapped(true) :: HNil

Assuming that you want to provide some custom encoding/decoding logic you should modify the signature

implicit def hconsWH[H, TI <: HList, TO <: HList](
  implicit
  tailWH: WrapperHelper.Aux[TI, TO]
): WrapperHelper.Aux[H :: TI, Wrapper[H] :: TO]

to something like

implicit def hconsWH[H, TI <: HList, TO <: HList](
  implicit
  thatThingINeedToLiftAToWrapperA: Encoder[A], // whatever is needed to lift A => Wrapper[A]
  tailWH: WrapperHelper.Aux[TI, TO]
): WrapperHelper.Aux[H :: TI, Wrapper[H] :: TO]

Even if you don't need the implementation it is still useful to define typeclass here, just to provide some implicit evidence that would resolve the type for you.

HList Int:: String:: Boolean:: HNil can be easily transformed into HList Wrapper[Int]:: Wrapper[String]:: Wrapper[Boolean]:: HNil using standard type class shapeless.ops.hlist.Mapped

implicitly[Mapped.Aux[Int :: String :: Boolean :: HNil, 
  Wrapper, 
  Wrapper[Int] :: Wrapper[String] :: Wrapper[Boolean] :: HNil]] // compiles

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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