This is a followup on Shapeless and annotations. The original question was asked in the context of Scala 2 and Shapeless2.
Some features from Shapeless2 were migrated to Shapeless3, such as annotations. The question is, how to migrate the solution to Shapeless3? especially the code around Poly2
?
Here is a copy/paste of the solution to be migrated to Shapeless3:
import shapeless.ops.hlist.{RightFolder, Zip}
import shapeless.{::, Annotations, Generic, HList, HNil, Lazy, Poly2}
import scala.annotation.StaticAnnotation
object App {
case class MyAnnotation(func: String) extends StaticAnnotation
object Collector extends Poly2 {
// implicit def myCase[ACC <: HList, E] = at[(E, Option[PII]), ACC] {
// case ((e, None), acc) => e :: acc
// case ((e, Some(MyAnnotation(func))), acc) => {
// println(func)
// e :: acc
// }
// }
implicit def someCase[ACC <: HList, E]: Case.Aux[(E, Some[MyAnnotation]), ACC, E :: ACC] = at {
case ((e, Some(MyAnnotation(func))), acc) =>
println(func)
e :: acc
}
implicit def noneCase[ACC <: HList, E]: Case.Aux[(E, None.type), ACC, E :: ACC] = at {
case ((e, None), acc) => e :: acc
}
}
trait Modifier[T] {
def modify(t: T): T
}
implicit def hListModifier[HL <: HList]: Modifier[HL] = identity(_)
// added as an example, you should replace this with your Modifier for HList
implicit def genericModifier[T, HL <: HList, AL <: HList, ZL <: HList](implicit
gen: Generic.Aux[T, HL],
ser: Lazy[Modifier[HL]],
annots: Annotations.Aux[MyAnnotation, T, AL],
zip: Zip.Aux[HL :: AL :: HNil, ZL],
rightFolder: RightFolder.Aux[ZL, HNil/*.type*/, Collector.type, HL /*added*/]
): Modifier[T] = new Modifier[T] {
override def modify(t: T): T = {
val generic = gen.to(t)
println(generic)
val annotations = annots()
println(annotations)
val zipped = zip(generic :: annotations :: HNil)
println(zipped)
val modified = zipped.foldRight(HNil : HNil /*added*/)(Collector)
println(modified)
val typed = gen.from(modified)
typed
}
}
case class Test(a: String, @MyAnnotation("sha1") b: String)
val test = Test("A", "B")
val modifier: Modifier[Test] = implicitly[Modifier[Test]]
def main(args: Array[String]): Unit = {
val test1 = modifier.modify(test) // prints "sha1"
println(test1) // Test(A,B)
}
}
In Scala 3 Tuple
is for HList
, Mirror
is for Generic
/LabelledGeneric
. There are polymorphic functions but they are parametric-polymorphism polymorphic, not ad-hoc-polymorphism polymorphic like Poly
.
Shapeless 3 has Annotations
, Typeable
and deriving tools (wrapping Mirror
).
It's not hard to implement missing pieces (Generic
, Coproduct
, Poly
, type classes etc.)
Scala 3 collection partitioning with subtypes
import shapeless3.deriving.Annotations
import scala.deriving.Mirror
import scala.util.NotGiven
import scala.annotation.StaticAnnotation
//================= GENERIC ====================
trait Generic[T] {
type Repr
def to(t: T): Repr
def from(r: Repr): T
}
object Generic {
type Aux[T, Repr0] = Generic[T] {type Repr = Repr0}
def instance[T, Repr0](f: T => Repr0, g: Repr0 => T): Aux[T, Repr0] =
new Generic[T] {
override type Repr = Repr0
override def to(t: T): Repr0 = f(t)
override def from(r: Repr0): T = g(r)
}
object ops {
extension[A] (a: A) {
def toRepr(using g: Generic[A]): g.Repr = g.to(a)
}
extension[Repr] (a: Repr) {
def to[A](using g: Generic.Aux[A, Repr]): A = g.from(a)
}
}
given [T <: Product](using
// ev: NotGiven[T <:< Tuple],
// ev1: NotGiven[T <:< Coproduct],
m: Mirror.ProductOf[T],
m1: Mirror.ProductOf[m.MirroredElemTypes]
): Aux[T, m.MirroredElemTypes] = instance(
m1.fromProduct(_),
m.fromProduct(_)
)
// given[T, C <: Coproduct](using
// // ev: NotGiven[T <:< Tuple],
// // ev1: NotGiven[T <:< Coproduct],
// m: Mirror.SumOf[T],
// ev2: Coproduct.ToCoproduct[m.MirroredElemTypes] =:= C
// ): Generic.Aux[T, C/*Coproduct.ToCoproduct[m.MirroredElemTypes]*/] = {
// instance(
// t => Coproduct.unsafeToCoproduct(m.ordinal(t), t).asInstanceOf[C],
// Coproduct.unsafeFromCoproduct(_).asInstanceOf[T]
// )
// }
}
//================= COPRODUCT ====================
//sealed trait Coproduct extends Product with Serializable
//sealed trait +:[+H, +T <: Coproduct] extends Coproduct
//final case class Inl[+H, +T <: Coproduct](head: H) extends (H +: T)
//final case class Inr[+H, +T <: Coproduct](tail: T) extends (H +: T)
//sealed trait CNil extends Coproduct
//
//object Coproduct {
// def unsafeToCoproduct(length: Int, value: Any): Coproduct =
// (0 until length).foldLeft[Coproduct](Inl(value))((c, _) => Inr(c))
//
// @scala.annotation.tailrec
// def unsafeFromCoproduct(c: Coproduct): Any = c match {
// case Inl(h) => h
// case Inr(c) => unsafeFromCoproduct(c)
// case _: CNil => sys.error("impossible")
// }
//
// type ToCoproduct[T <: Tuple] <: Coproduct = T match {
// case EmptyTuple => CNil
// case h *: t => h +: ToCoproduct[t]
// }
//
// type ToTuple[C <: Coproduct] <: Tuple = C match {
// case CNil => EmptyTuple
// case h +: t => h *: ToTuple[t]
// }
//}
//================= POLY ====================
trait Cases {
type Case1[Fn, A] = poly.Case[Fn, A *: EmptyTuple]
object Case1 {
type Aux[Fn, A, Result] = poly.Case.Aux[Fn, A *: EmptyTuple, Result]
def apply[Fn, A, Result](fn: A => Result): Case1.Aux[Fn, A, Result] =
poly.Case { case a *: EmptyTuple => fn(a) }
}
type Case2[Fn, A, B] = poly.Case[Fn, A *: B *: EmptyTuple]
object Case2 {
type Aux[Fn, A, B, Result] = poly.Case.Aux[Fn, A *: B *: EmptyTuple, Result]
def apply[Fn, A, B, Result](fn: (A, B) => Result): Case2.Aux[Fn, A, B, Result] =
poly.Case { case a *: b *: EmptyTuple => fn(a, b) }
}
}
trait CaseInst {
given inst1[Fn <: Poly, A, Res]: Conversion[poly.Case.Aux[Fn, A *: EmptyTuple, Res], A => Res] =
cse => a => cse.value(a *: EmptyTuple)
given inst2[Fn <: Poly, A, B, Res]: Conversion[poly.Case.Aux[Fn, A *: B *: EmptyTuple, Res], (A, B) => Res] =
cse => (a, b) => cse.value(a *: b *: EmptyTuple)
}
object poly extends Cases {
trait Case[P, L <: Tuple] {
type Result
val value: L => Result
def apply(t: L): Result = value(t)
def apply()(using ev: EmptyTuple =:= L): Result = value(EmptyTuple)
def apply[T](t: T)(using ev: (T *: EmptyTuple) =:= L): Result = value(t *: EmptyTuple)
def apply[T, U](t: T, u: U)(using ev: (T *: U *: EmptyTuple) =:= L): Result = value(t *: u *: EmptyTuple)
}
object Case extends CaseInst {
type Aux[P, L <: Tuple, Result0] = Case[P, L] {type Result = Result0}
def apply[P, L <: Tuple, R](v: L => R): Aux[P, L, R] = new Case[P, L] {
type Result = R
val value = v
}
}
}
trait PolyApply {
type λ <: Singleton
def apply[A](a: A)(using cse: poly.Case[λ, A *: EmptyTuple]): cse.Result = cse(a *: EmptyTuple)
def apply[A, B](a: A, b: B)(using cse: poly.Case[λ, A *: B *: EmptyTuple]): cse.Result = cse(a *: b *: EmptyTuple)
}
trait Poly extends PolyApply {
type λ = this.type
type ProductCase[L <: Tuple] = poly.Case[this.type, L]
object ProductCase extends Serializable {
type Aux[L <: Tuple, Result0] = ProductCase[L] {type Result = Result0}
def apply[L <: Tuple, R](v: L => R) = new ProductCase[L] {
type Result = R
val value = v
}
}
def apply[R](using c: ProductCase.Aux[EmptyTuple, R]): R = c()
}
trait PolyInst {
implicit def inst0(p: Poly)(implicit cse: p.ProductCase[EmptyTuple]): cse.Result = cse()
implicit def inst1[A](fn: Poly)(implicit cse: fn.ProductCase[A *: EmptyTuple]): A => cse.Result =
a => cse(a *: EmptyTuple)
implicit def inst2[A, B](fn: Poly)(implicit cse: fn.ProductCase[A *: B *: EmptyTuple]): (A, B) => cse.Result =
(a, b) => cse(a *: b *: EmptyTuple)
}
object Poly extends PolyInst
trait Poly0 extends Poly {
type Case0[T] = ProductCase.Aux[EmptyTuple, T]
def at[T](t: T) = new ProductCase[EmptyTuple] {
type Result = T
val value = _ => t
}
}
trait Poly1 extends Poly { self =>
type Case[A] = poly.Case[self.type, A *: EmptyTuple]
object Case {
type Aux[A, Result0] = poly.Case.Aux[self.type, A *: EmptyTuple, Result0]
}
class CaseBuilder1[A] {
def apply[Res](fn: A => Res): Case.Aux[A, Res] = poly.Case { case a *: EmptyTuple => fn(a) }
}
def at[A]: CaseBuilder1[A] = new CaseBuilder1[A]
}
trait Poly2 extends Poly { self =>
type Case[A, B] = poly.Case[self.type, A *: B *: EmptyTuple]
object Case {
type Aux[A, B, Result0] = poly.Case.Aux[self.type, A *: B *: EmptyTuple, Result0]
}
class CaseBuilder2[A, B] {
def apply[Res](fn: (A, B) => Res): Case.Aux[A, B, Res] = poly.Case { case a *: b *: EmptyTuple => fn(a, b) }
}
def at[A, B]: CaseBuilder2[A, B] = new CaseBuilder2[A, B]
}
//================= TYPE CLASSES ====================
trait DepFn0 {
type Out
def apply(): Out
}
trait DepFn1[T] {
type Out
def apply(t: T): Out
}
trait DepFn2[T, U] {
type Out
def apply(t: T, u: U): Out
}
trait ConstMapper[C, L <: Tuple] extends DepFn2[C, L] {
type Out <: Tuple
}
object ConstMapper {
def apply[C, L <: Tuple](using mapper: ConstMapper[C, L]): Aux[C, L, mapper.Out] = mapper
type Aux[C, L <: Tuple, Out0 <: Tuple] = ConstMapper[C, L] {type Out = Out0}
given hnilConstMapper[C]: Aux[C, EmptyTuple, EmptyTuple] =
new ConstMapper[C, EmptyTuple] {
type Out = EmptyTuple
def apply(c: C, l: EmptyTuple): Out = l
}
given hlistConstMapper[H, T <: Tuple, C, OutT <: Tuple]
(using mct: ConstMapper.Aux[C, T, OutT]): Aux[C, H *: T, C *: OutT] =
new ConstMapper[C, H *: T] {
type Out = C *: OutT
def apply(c: C, l: H *: T): Out = c *: mct(c, l.tail)
}
}
trait ZipOne[H <: Tuple, T <: Tuple] extends DepFn2[H, T] {
type Out <: Tuple
}
object ZipOne extends LowPriorityZipOne {
given zipOne0: Aux[EmptyTuple, EmptyTuple, EmptyTuple] =
new ZipOne[EmptyTuple, EmptyTuple] {
type Out = EmptyTuple
def apply(h: EmptyTuple, t: EmptyTuple): Out = EmptyTuple
}
given zipOne3[H, T <: Tuple]: Aux[H *: EmptyTuple, T *: EmptyTuple, (H *: T) *: EmptyTuple] =
new ZipOne[H *: EmptyTuple, T *: EmptyTuple] {
type Out = (H *: T) *: EmptyTuple
def apply(h: H *: EmptyTuple, t: T *: EmptyTuple): Out = (h.head *: t.head) *: EmptyTuple
}
}
trait LowPriorityZipOne {
def apply[H <: Tuple, T <: Tuple](using zip: ZipOne[H, T]): Aux[H, T, zip.Out] = zip
type Aux[H <: Tuple, T <: Tuple, Out0 <: Tuple] = ZipOne[H, T] {type Out = Out0}
given zipOne1[H <: Tuple]: Aux[H, EmptyTuple, EmptyTuple] =
new ZipOne[H, EmptyTuple] {
type Out = EmptyTuple
def apply(h: H, t: EmptyTuple): Out = EmptyTuple
}
given zipOne2[T <: Tuple]: Aux[EmptyTuple, T, EmptyTuple] =
new ZipOne[EmptyTuple, T] {
type Out = EmptyTuple
def apply(h: EmptyTuple, t: T): Out = EmptyTuple
}
given zipOne4[HH, HT <: Tuple, TH <: Tuple, TT <: Tuple, ZotOut <: Tuple]
(using zot: ZipOne.Aux[HT, TT, ZotOut], ev: Tuple.Head[TH *: TT] =:= TH /*???*/): Aux[HH *: HT, TH *: TT, (HH *: TH) *: ZotOut] =
new ZipOne[HH *: HT, TH *: TT] {
type Out = (HH *: TH) *: ZotOut
def apply(h: HH *: HT, t: TH *: TT): Out = (h.head *: ev(t.head)) *: zot(h.tail, t.tail)
}
}
trait Transposer[L <: Tuple] extends DepFn1[L] {
type Out <: Tuple
}
object Transposer {
def apply[L <: Tuple](using transposer: Transposer[L]): Aux[L, transposer.Out] = transposer
type Aux[L <: Tuple, Out0 <: Tuple] = Transposer[L] {type Out = Out0}
given hnilTransposer: Aux[EmptyTuple, EmptyTuple] =
new Transposer[EmptyTuple] {
type Out = EmptyTuple
def apply(l: EmptyTuple): Out = l
}
given hlistTransposer1[H <: Tuple, MC <: Tuple, Out0 <: Tuple]
(using mc: ConstMapper.Aux[EmptyTuple, H, MC], zo: ZipOne.Aux[H, MC, Out0]): Aux[H *: EmptyTuple, Out0] =
new Transposer[H *: EmptyTuple] {
type Out = Out0
def apply(l: H *: EmptyTuple): Out = zo(l.head, mc(EmptyTuple, l.head))
}
given hlistTransposer2[H <: Tuple, TH <: Tuple, TT <: Tuple, OutT <: Tuple, Out0 <: Tuple]
(using tt: Aux[TH *: TT, OutT], zo: ZipOne.Aux[H, OutT, Out0]): Aux[H *: TH *: TT, Out0] =
new Transposer[H *: TH *: TT] {
type Out = Out0
def apply(l: H *: TH *: TT): Out = zo(l.head, tt(l.tail))
}
}
trait Zip[L <: Tuple] extends DepFn1[L] {
type Out <: Tuple
}
object Zip {
def apply[L <: Tuple](using zip: Zip[L]): Aux[L, zip.Out] = zip
type Aux[L <: Tuple, Out0 <: Tuple] = Zip[L] {type Out = Out0}
given zipper[L <: Tuple, OutT <: Tuple]
(using
transposer: Transposer.Aux[L, OutT]
): Aux[L, OutT] =
new Zip[L] {
type Out = OutT
def apply(l: L): Out = l.transpose
}
}
extension [L <: Tuple](l: L) {
def transpose(using transpose: Transposer[L]): transpose.Out = transpose(l)
def foldRight[R](z : R)(op : Poly)(using folder: RightFolder[L, R, op.type]): folder.Out = folder(l, z)
}
trait RightFolder[L <: Tuple, In, HF] extends DepFn2[L, In]
object RightFolder {
def apply[L <: Tuple, In, F](using folder: RightFolder[L, In, F]): Aux[L, In, F, folder.Out] = folder
type Aux[L <: Tuple, In, HF, Out0] = RightFolder[L, In, HF] {type Out = Out0}
given hnilRightFolder[In, HF]: Aux[EmptyTuple, In, HF, In] =
new RightFolder[EmptyTuple, In, HF] {
type Out = In
def apply(l: EmptyTuple, in: In): Out = in
}
given hlistRightFolder[H, T <: Tuple, In, HF, OutT]
(using ft: RightFolder.Aux[T, In, HF, OutT], f: poly.Case2[HF, H, OutT]): Aux[H *: T, In, HF, f.Result] =
new RightFolder[H *: T, In, HF] {
type Out = f.Result
def apply(l: H *: T, in: In): Out = f(l.head, ft(l.tail, in))
}
}
//================= YOUR SETTING ====================
case class MyAnnotation(func: String) extends StaticAnnotation
object Collector extends Poly2 {
given [ACC <: Tuple, E]: Case.Aux[(E, Some[MyAnnotation]), ACC, E *: ACC] = at {
case ((e, Some(MyAnnotation(func))), acc) =>
println(func)
e *: acc
}
given [ACC <: Tuple, E]: Case.Aux[(E, None.type), ACC, E *: ACC] = at {
case ((e, None), acc) => e *: acc
}
}
trait Modifier[T] {
def modify(t: T): T
}
given hListModifier[HL <: Tuple]: Modifier[HL] = identity(_)
// added as an example, you should replace this with your Modifier for HList
given genericModifier[T, HL <: Tuple, AL <: Tuple, ZL <: Tuple](using
gen: Generic.Aux[T, HL],
ser: /*Lazy[*/Modifier[HL]/*]*/,
annots: Annotations.Aux[MyAnnotation, T, AL],
zip: Zip.Aux[HL *: AL *: EmptyTuple, ZL],
rightFolder: RightFolder.Aux[ZL, EmptyTuple, Collector.type, HL]
): Modifier[T] = new Modifier[T] {
override def modify(t: T): T = {
val generic = gen.to(t)
println(generic)
val annotations = annots()
println(annotations)
val zipped = zip(generic *: annotations *: EmptyTuple)
println(zipped)
val modified = zipped.foldRight(EmptyTuple)(Collector)
println(modified)
val typed = gen.from(modified)
typed
}
}
case class Test(a: String, @MyAnnotation("sha1") b: String)
val test = Test("A", "B")
val modifier: Modifier[Test] = summon[Modifier[Test]]
@main def run = {
val test1 = modifier.modify(test) // prints "sha1"
println(test1) // Test(A,B)
}
Maybe some of type classes can be replaced with match types or compile-time calculations.
It can be tricky to implement Lazy
. It's not clear whether it's needed. There are by-name implicits but they are not equivalent to Lazy
(1 2). In principle, Lazy
can be implemented in Scala 3 since compiler internals for implicits in Scala 3 are similar to those in Scala 2 (1 2 3).
I would like to have some function applied to fields in a case class, that are annotated with
MyAnnotation
. The idea is to transform typeT
into its generic representation, extract annotations, zip, fold right (or left) to reconstruct a generic representation and finally get back to typeT
.
Here is simpler solution for Scala 3
import shapeless3.deriving.Annotations
import scala.annotation.StaticAnnotation
import scala.deriving.Mirror
case class MyAnnotation(func: String) extends StaticAnnotation
case class Test(a: String, @MyAnnotation("sha1") b: String)
def fold[Tup <: Tuple, Z, F[_, _]](tup: Tup, z: Z, f: [A, B] => (A, B) => F[A, B]): Tuple.Fold[Tup, Z, F] = tup match {
case _: EmptyTuple => z
case tup: (h *: t) => f[h, Tuple.Fold[t, Z, F]](tup.head, fold[t, Z, F](tup.tail, z, f))
}
type Collector[A, B <: Tuple] = A match {
case (a, Some[MyAnnotation]) => a *: B
case (a, None.type) => a *: B
}
transparent inline def foo[T <: Product](t: T)(using
m: Mirror.ProductOf[T],
m1: Mirror.ProductOf[m.MirroredElemTypes] {type MirroredElemTypes = m.MirroredElemTypes},
ann: Annotations[MyAnnotation, T]
): Any = {
val tuple: m.MirroredElemTypes = m1.fromProduct(t)
println(s"tuple=$tuple")
val annotations: ann.Out = ann()
println(s"annotations=$annotations")
type Zipped = Tuple.Zip[m.MirroredElemTypes, ann.Out]
val zipped: Zipped = tuple.zip(annotations)
println(s"zipped=$zipped")
def collector[A, B <: Tuple](x: A, y: B): Collector[A, B] = (x match {
case (a, Some(annot)) =>
println(s"annot=$annot")
a *: y
case (a, None) =>
a *: y
}).asInstanceOf[Collector[A, B]]
type Folded = Tuple.Fold[Zipped, EmptyTuple, [a, b] =>> Collector[a, b & Tuple]]
val folded: Folded = fold[Zipped, EmptyTuple, [a, b] =>> Collector[a, b & Tuple]](
zipped,
EmptyTuple,
[a, b] => (x: a, y: b) => collector(x, y.asInstanceOf[b & Tuple])
)
m.fromProduct(folded.asInstanceOf[Folded & Product])
}
val res: Test = foo(Test("aa", "bb")) // Test(aa,bb)
// tuple=(aa,bb)
// annotations=(None,Some(MyAnnotation(sha1)))
// zipped=((aa,None),(bb,Some(MyAnnotation(sha1))))
// annot=MyAnnotation(sha1)