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

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
eitaporra
  • 51
  • 7
  • 2
    There are resources: https://underscore.io/books/shapeless-guide/ (kinda official) or https://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 of`Generic[Data]`, `A =:= Data` - and pass it to `implicit def forTimestampedData[A](implicit a: TypeClass[A]): TypeClass[TimestampedData[A]] = /* your implementation */`. – Mateusz Kubuszok Nov 14 '22 at 10:47
  • @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? – eitaporra Nov 14 '22 at 11:27
  • 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. – Mateusz Kubuszok Nov 14 '22 at 11:54
  • @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 lot – eitaporra Nov 17 '22 at 13:13

1 Answers1

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

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • 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? – eitaporra Nov 17 '22 at 13:14
  • @eitaporra Resources for Shapeless are: the book https://underscore.io/books/shapeless-guide/ , wiki https://github.com/milessabin/shapeless/wiki/Feature-overview:-shapeless-2.0.0 , examples https://github.com/milessabin/shapeless/tree/main/examples/src/main/scala/shapeless/examples , tests https://github.com/milessabin/shapeless/tree/main/core/shared/src/test/scala/shapeless If you don't know what a type class does you go to tests and see. – Dmytro Mitin Nov 17 '22 at 14:25
  • @eitaporra For example `Mapped` https://github.com/milessabin/shapeless/blob/v2.3.10/core/src/test/scala/shapeless/hlist.scala#L211-L219 , `Inject` https://github.com/milessabin/shapeless/blob/v2.3.10/core/src/test/scala/shapeless/coproduct.scala#L58-L75 https://github.com/milessabin/shapeless/blob/v2.3.10/core/src/test/scala/shapeless/coproduct.scala#L1960-L1975 `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. – Dmytro Mitin Nov 17 '22 at 14:25
  • 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 them – eitaporra Nov 17 '22 at 14:36