Reputation: 767
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?
Upvotes: 2
Views: 114
Reputation: 51723
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
Upvotes: 1
Reputation: 27595
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.
Upvotes: 1