scalatypeclassimplicitshapelessgeneric-derivation

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


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


Solution

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