How to derive a Generic.Aux if the case class has a type parameter - Shapeless

146 views Asked by At

given:

sealed trait Data
final case class Foo() extends Data
final case class Bar() extends Data

final case class TimestampedData[A <: Data](data: A, timestamp: Long)

Is there a succint way to generate, for example, a Generic.Aux that will take a

(A, Long) where A <: Data

and out this Coproduct:

TimestampedData[Foo] :+: TimestampedData[Bar] :+: CNil

(Generic.Aux[(A, Long), TimestampedData[Foo] :+: TimestampedData[Bar] :+: CNil])

?

Unfortunately, since I don't know much generic programming and because of the lack of resources, I haven't tried much. I'm not even sure how to approach this problem.

Thanks

1

There are 1 answers

4
Dmytro Mitin On BEST ANSWER

You can try a method with PartiallyApplied pattern

import shapeless.{Coproduct, DepFn2, Generic, HList}
import shapeless.ops.coproduct.{Inject, ToHList}
import shapeless.ops.hlist.{Mapped, ToCoproduct}

def toTimestamped[A <: Data] = new PartiallyApplied[A]

class PartiallyApplied[A <: Data] {
  def apply[C  <: Coproduct, 
            L  <: HList, 
            L1 <: HList, 
            C1 <: Coproduct](data: A, timestamp: Long)(implicit
    generic: Generic.Aux[Data, C],
    toHList: ToHList.Aux[C, L],
    mapped: Mapped.Aux[L, λ[A => TimestampedData[A with Data]], L1],
    toCoproduct: ToCoproduct.Aux[L1, C1],
    inject: Inject[C1, TimestampedData[A]],
  ): C1 = inject(TimestampedData[A](data, timestamp))
}
val x = toTimestamped(Foo(), 1L) // Inr(Inl(TimestampedData(Foo(),1)))
val y = toTimestamped(Bar(), 1L) // Inl(TimestampedData(Bar(),1))
type Coprod = TimestampedData[Bar] :+: TimestampedData[Foo] :+: CNil
x: Coprod // compiles
y: Coprod // compiles

or a typeclass 1 2 3 4 5 (generally, a more flexible solution than a method although now there seem to be no advantages over a method because there is the only instance of the type class)

trait ToTimestamped[A <: Data] extends DepFn2[A, Long] {
  type Out <: Coproduct
}
object ToTimestamped {
  type Aux[A <: Data, Out0 <: Coproduct] = ToTimestamped[A] { type Out = Out0 }
  def instance[A <: Data, Out0 <: Coproduct](f: (A, Long) => Out0): Aux[A, Out0] =
    new ToTimestamped[A] {
      override type Out = Out0
      override def apply(data: A, timestamp: Long): Out0 = f(data, timestamp)
    }

  implicit def mkToTimestamped[A  <: Data, 
                               C  <: Coproduct, 
                               L  <: HList, 
                               L1 <: HList, 
                               C1 <: Coproduct](implicit
    generic: Generic.Aux[Data, C],
    toHList: ToHList.Aux[C, L],
    mapped: Mapped.Aux[L, λ[A => TimestampedData[A with Data]], L1],
    toCoproduct: ToCoproduct.Aux[L1, C1],
    inject: Inject[C1, TimestampedData[A]],
  ): Aux[A, C1] =
    instance((data, timestamp) => inject(TimestampedData[A](data, timestamp)))
}

def toTimestamped[A <: Data](data: A, timestamp: Long)(implicit
  toTimestampedInst: ToTimestamped[A]
): toTimestampedInst.Out = toTimestampedInst(data, timestamp)

Testing:

val x = toTimestamped(Foo(), 1L) // Inr(Inl(TimestampedData(Foo(),1)))
val y = toTimestamped(Bar(), 1L) // Inl(TimestampedData(Bar(),1))
type Coprod = TimestampedData[Bar] :+: TimestampedData[Foo] :+: CNil
implicitly[ToTimestamped.Aux[Foo, Coprod]] // compiles
x: Coprod // compiles
y: Coprod // compiles

In Shapeless there is Mapped for HList but not Coproduct, so I had to transform on type level Coproduct to HList and back.

λ[A => ...] is kind-projector syntax. Mapped accepts a type constructor F[_] but TimestampedData is upper-bounded F[_ <: Data], so I had to use a type lambda with intersection type (with).