scalatypeclassimplicitshapelessscala-3

Shapeless3 and annotations


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

Solution

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


    Shapeless and annotations

    I would like to have some function applied to fields in a case class, that are annotated with MyAnnotation. The idea is to transform type T into its generic representation, extract annotations, zip, fold right (or left) to reconstruct a generic representation and finally get back to type T.

    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)