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

1

1

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

Vitiated answered 14/11, 2022 at 10:27 Comment(4)
There are resources: underscore.io/books/shapeless-guide (kinda official) or github.com/krzemin/scalawave-typelevel-workshop (my colleague's workshop). You wouldn't write TimestampedData[Foo] :+: TimestampedData[Bar] :+: CNil, you would define a type class TypeClass[A], then provide an instance of type class TypeClass[A] - which in your case could be generated with the help ofGeneric[Data], A =:= Data - and pass it to implicit def forTimestampedData[A](implicit a: TypeClass[A]): TypeClass[TimestampedData[A]] = /* your implementation */.Stevestevedore
@MateuszKubuszok So, essentially, this type class will be used only to map a Data to TimestampedData? Because I can already derive a Generic[Data] easily (which gets me Foo :+: Bar :+: CNil). Then I map those to TimestampedData? What about the timestamp?Vitiated
No, this type class will be used to: 1. define some behavior, 2. give you an interface to define how behavior for smaller parts can be combined of behavior of bigger parts. If you define yourself how to e.g. handle TimestampedData[A] using behavior for A, then behavior for Data can be derived using Coproduct and used in TimestampedData[A] behavior. If you want o derive behavior for TimestampedData[A] as well, you can use derivation for HList. But it all requires you to know what behavior you actually want and how you want it composed.Stevestevedore
@MateuszKubuszok After a lot of time reading the shapeless guide and looking at some examples, I finally know what you mean by "behaviour". Thanks a lotVitiated
S
0

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

Spiritualize answered 14/11, 2022 at 15:55 Comment(4)
Awesome, thanks. Took me a lot of time to understand what's happening but I get most of it now. I like how you can do pretty much a composition using the Aux pattern, really cool. I still have to get to know what Mapped and Inject does exactly. Do you have any resources?Vitiated
@Vitiated Resources for Shapeless are: the book underscore.io/books/shapeless-guide , wiki github.com/milessabin/shapeless/wiki/… , examples github.com/milessabin/shapeless/tree/main/examples/src/main/… , tests github.com/milessabin/shapeless/tree/main/core/shared/src/test/… If you don't know what a type class does you go to tests and see.Spiritualize
@Vitiated For example Mapped github.com/milessabin/shapeless/blob/v2.3.10/core/src/test/… , Inject github.com/milessabin/shapeless/blob/v2.3.10/core/src/test/… github.com/milessabin/shapeless/blob/v2.3.10/core/src/test/… Mapped transforms A :: B :: C :: HNil into F[A] :: F[B] :: F[C] :: HNil on type level, Inject transforms A, B or C into A :+: B :+: C :+: CNil on both type and value level.Spiritualize
I'm aware of type classes; I think my issue was mostly dependent types and the Aux pattern. Now everything clicks much better but still a lot to learn. Thanks a lot for the links, I'll make sure to study themVitiated

© 2022 - 2024 — McMap. All rights reserved.