scalascala-reflect

Getting Case Class definition which points to another Case Class


I am looking at getting case class definitions.

From SO I gleaned this practice as per Get field names list from case class, the answer using reflection by Dia Kharrat.

Some experimenting in which I have a case class referring to another case class, nested. Can we get the metadata expanded easily in some way?

import scala.collection.mutable.ArrayBuffer

case class MyChgClass(b: Option[String], c: Option[String], d: Option[String])
case class MyFullClass(k: Int, b: String, c: String, d: String)
case class MyEndClass(id: Int, after: MyFullClass)

def classAccessors[T: TypeTag]: List[MethodSymbol] = typeOf[T].members.collect {
 case m: MethodSymbol if m.isCaseAccessor => m
}.toList

val z1 = classAccessors[MyChgClass]
val z2 = classAccessors[MyFullClass]
val z3 = classAccessors[MyEndClass]

returns:

z1: List[reflect.runtime.universe.MethodSymbol] = List(value d, value c, value b)
z2: List[reflect.runtime.universe.MethodSymbol] = List(value d, value c, value b, value k)
z3: List[reflect.runtime.universe.MethodSymbol] = List(value after, value id)

So:

  1. Looking to expand the case class MyEndClass.
  2. The option aspect appears not not been supplied. Possible?

Solution

    1. The option aspect appears not not been supplied. Possible?

    Are you looking for .name and .typeSignature?

    val z1 = classAccessors[MyChgClass]
    val z2 = classAccessors[MyFullClass]
    val z3 = classAccessors[MyEndClass]
    
    z1.map(_.name) // List(d, c, b)
    z1.map(_.typeSignature) // List(Option[String], Option[String], Option[String])
    z2.map(_.name) // List(d, c, b, k)
    z2.map(_.typeSignature) // List(String, String, String, Int)
    z3.map(_.name) // List(after, id)
    z3.map(_.typeSignature) // List(MyFullClass, Int)
    

    If your classes are known at compile time it would make sense to use compile-time reflection i.e. macros rather than runtime reflection

    import scala.language.experimental.macros
    import scala.reflect.macros.blackbox
    
    def classAccessors[T]: List[(String, String)] = macro classAccessorsImpl[T]
    
    def classAccessorsImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Tree = {
      import c.universe._
      val pairs = weakTypeOf[T].members.collect {
        case m: MethodSymbol if m.isCaseAccessor => m
      }.map(m => (m.name.toString, m.typeSignature.toString))
      q"List.apply[(String, String)](..$pairs)"
    }
    
    // in a different subproject
    classAccessors[MyChgClass] // List((d,Option[String]), (c,Option[String]), (b,Option[String]))
    classAccessors[MyFullClass] // List((d,String), (c,String), (b,String), (k,Int))
    classAccessors[MyEndClass] // List((after,MyFullClass), (id,Int))
    

    Even better would be to use one of libraries encapsulating those macros into some type classes. In Shapeless the type class giving access to names and types of case-class fields is LabelledGeneric

    // libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.10"
    import shapeless.labelled.FieldType
    import shapeless.ops.hlist.{FillWith, Mapper, ToList}
    import shapeless.{HList, LabelledGeneric, Poly0, Poly1, Typeable, Witness}
    
    object fieldNamesAndTypesPoly extends Poly1 {
      implicit def cse[K <: Symbol, V](implicit
        witness: Witness.Aux[K],
        typeable: Typeable[V]
      ): Case.Aux[FieldType[K, V], (String, String)] =
        at(_ => (witness.value.name, typeable.describe))
    }
    
    object nullPoly extends Poly0 {
      implicit def cse[A]: Case0[A] = at(null.asInstanceOf[A])
    }
    
    def classAccessors[T] = new PartiallyApplied[T]
    
    class PartiallyApplied[T] {
      def apply[L <: HList, L1 <: HList]()(implicit
        labelledGeneric: LabelledGeneric.Aux[T, L],
        fillWith: FillWith[nullPoly.type, L],
        mapper: Mapper.Aux[fieldNamesAndTypesPoly.type, L, L1],
        toList: ToList[L1, (String, String)]
      ): List[(String, String)] = toList(mapper(fillWith()))
    }
    
    classAccessors[MyChgClass]() // List((b,Option[String]), (c,Option[String]), (d,Option[String]))
    classAccessors[MyFullClass]() // List((k,Int), (b,String), (c,String), (d,String))
    classAccessors[MyEndClass]() // List((id,Int), (after,MyFullClass))
    

    1. Looking to expand the case class MyEndClass.

    You can try deep versions of the type classes LabelledGeneric, Mapper etc.

    import shapeless.labelled.{FieldType, field}
    import shapeless.{::, DepFn0, DepFn1, HList, HNil, LabelledGeneric, Poly0, Poly1, Typeable, Witness, poly}
    
    trait DeepLabelledGeneric[T <: Product] {
      type Repr <: HList
      def to(t: T): Repr
      def from(r: Repr): T
    }
    
    object DeepLabelledGeneric {
      type Aux[T <: Product, Repr0 <: HList] = DeepLabelledGeneric[T] {type Repr = Repr0}
      def instance[T <: Product, Repr0 <: HList](f: T => Repr0, g: Repr0 => T): Aux[T, Repr0] = new DeepLabelledGeneric[T] {
        override type Repr = Repr0
        override def to(t: T): Repr = f(t)
        override def from(r: Repr): T = g(r)
      }
    
      implicit def deepGeneric[A <: Product, L <: HList, L1 <: HList](implicit
        labelledGeneric: LabelledGeneric.Aux[A, L],
        hListDeepLabelledGeneric: HListDeepLabelledGeneric.Aux[L, L1]
      ): Aux[A, L1] = instance(a => hListDeepLabelledGeneric.to(labelledGeneric.to(a)), l1 => labelledGeneric.from(hListDeepLabelledGeneric.from(l1)))
    }
    
    trait HListDeepLabelledGeneric[T <: HList] {
      type Repr <: HList
      def to(t: T): Repr
      def from(r: Repr): T
    }
    
    trait LowPriorityHListDeepLabelledGeneric {
      type Aux[T <: HList, Repr0 <: HList] = HListDeepLabelledGeneric[T] {type Repr = Repr0}
    
      def instance[T <: HList, Repr0 <: HList](f: T => Repr0, g: Repr0 => T): Aux[T, Repr0] = new HListDeepLabelledGeneric[T] {
        override type Repr = Repr0
        override def to(t: T): Repr = f(t)
        override def from(r: Repr): T = g(r)
      }
    
      implicit def headNotCaseClass[H, T <: HList, T_hListDeepLGen <: HList](implicit
        tailHListDeepLabelledGeneric: HListDeepLabelledGeneric.Aux[T, T_hListDeepLGen]
      ): Aux[H :: T, H :: T_hListDeepLGen] = instance({
        case h :: t => h :: tailHListDeepLabelledGeneric.to(t)
      }, {
        case h :: t => h :: tailHListDeepLabelledGeneric.from(t)
      })
    }
    
    object HListDeepLabelledGeneric extends LowPriorityHListDeepLabelledGeneric {
      implicit val hNil: Aux[HNil, HNil] = instance(identity, identity)
    
      implicit def headCaseClass[K <: Symbol, H <: Product, T <: HList, H_deepLGen <: HList, T_hListDeepLGen <: HList](implicit
        headDeepLabelledGeneric: DeepLabelledGeneric.Aux[H, H_deepLGen],
        tailHListDeepLabelledGeneric: HListDeepLabelledGeneric.Aux[T, T_hListDeepLGen]
      ): Aux[FieldType[K, H] :: T, FieldType[K, H_deepLGen] :: T_hListDeepLGen] = instance({
        case h :: t => field[K](headDeepLabelledGeneric.to(h)) :: tailHListDeepLabelledGeneric.to(t)
      }, {
        case h :: t => field[K](headDeepLabelledGeneric.from(h)) :: tailHListDeepLabelledGeneric.from(t)
      })
    }
    
    
    trait DeepMapper[P <: Poly1, In <: HList] extends DepFn1[In] {
      type Out <: HList
    }
    
    trait LowPriorityDeepMapper {
      def apply[P <: Poly1, L <: HList](implicit deepMapper: DeepMapper[P, L]): Aux[P, L, deepMapper.Out] = deepMapper
      type Aux[P <: Poly1, In <: HList, Out0 <: HList] = DeepMapper[P, In] {type Out = Out0}
      def instance[P <: Poly1, In <: HList, Out0 <: HList](f: In => Out0): Aux[P, In, Out0] = new DeepMapper[P, In] {
        override type Out = Out0
        override def apply(t: In): Out = f(t)
      }
    
      implicit def headNotHList[P <: Poly1, H, T <: HList](implicit
        headCase: poly.Case1[P, H],
        tailDeepMapper: DeepMapper[P, T]
      ): Aux[P, H :: T, headCase.Result :: tailDeepMapper.Out] =
        instance(l => headCase(l.head) :: tailDeepMapper(l.tail))
    }
    
    object DeepMapper extends LowPriorityDeepMapper {
      implicit def hNil[P <: Poly1]: Aux[P, HNil, HNil] = instance(_ => HNil)
    
      implicit def headHList[P <: Poly1, K <: Symbol, H <: HList, H_deepMap <: HList, T <: HList](implicit
        headDeepMapper: DeepMapper.Aux[P, H, H_deepMap],
        headCase: poly.Case1[P, FieldType[K, H_deepMap]], // apply poly one more time
        tailDeepMapper: DeepMapper[P, T]
      ): Aux[P, FieldType[K, H] :: T, headCase.Result :: tailDeepMapper.Out] =
        instance(l => headCase(field[K](headDeepMapper(l.head))) :: tailDeepMapper(l.tail))
    }
    
    trait DeepFillWith[P <: Poly0, L <: HList] extends DepFn0 {
      type Out = L
    }
    
    trait LowPriorityDeepFillWith {
      def apply[P <: Poly0, L <: HList](implicit deepFillWith: DeepFillWith[P, L]): DeepFillWith[P, L] = deepFillWith
      def instance[P <: Poly0, L <: HList](f: => L): DeepFillWith[P, L] = new DeepFillWith[P, L] {
        override def apply(): L = f
      }
    
      implicit def headNotHList[P <: Poly0, H, T <: HList](implicit
        headCase: poly.Case0.Aux[P, H],
        tailDeepFillWith: DeepFillWith[P, T]
      ): DeepFillWith[P, H :: T] =
        instance(headCase() :: tailDeepFillWith())
    }
    
    object DeepFillWith extends LowPriorityDeepFillWith {
      implicit def hNil[P <: Poly0]: DeepFillWith[P, HNil] = instance(HNil)
    
      implicit def headHList[P <: Poly0, K <: Symbol, H <: HList, T <: HList](implicit
        headDeepFillWith: DeepFillWith[P, H],
        tailDeepFillWith: DeepFillWith[P, T]
      ): DeepFillWith[P, FieldType[K, H] :: T] =
        instance(field[K](headDeepFillWith()) :: tailDeepFillWith())
    }
    
    trait LowPriorityFieldNamesAndTypesPoly extends Poly1 {
      implicit def notHListCase[K <: Symbol, V](implicit
        witness: Witness.Aux[K],
        typeable: Typeable[V]
      ): Case.Aux[FieldType[K, V], (String, String)] =
        at(_ => (witness.value.name, typeable.describe))
    }
    
    object fieldNamesAndTypesPoly extends LowPriorityFieldNamesAndTypesPoly {
      implicit def hListCase[K <: Symbol, V <: HList](implicit
        witness: Witness.Aux[K],
      ): Case.Aux[FieldType[K, V], (String, V)] =
        at(v => (witness.value.name, v)) // for DeepMapper "applying this poly one more time"
    }
    
    object nullPoly extends Poly0 {
      implicit def cse[A]: Case0[A] = at(null.asInstanceOf[A])
    }
    
    def classAccessors[T <: Product] = new PartiallyApplied[T]
    
    class PartiallyApplied[T <: Product] {
      def apply[L <: HList]()(implicit
        deepLabelledGeneric: DeepLabelledGeneric.Aux[T, L],
        deepFillWith: DeepFillWith[nullPoly.type, L],
        deepMapper: DeepMapper[fieldNamesAndTypesPoly.type, L],
      ): deepMapper.Out = deepMapper(deepFillWith())
    }
    
    classAccessors[MyChgClass]() // (b,Option[String]) :: (c,Option[String]) :: (d,Option[String]) :: HNil
    classAccessors[MyFullClass]() // (k,Int) :: (b,String) :: (c,String) :: (d,String) :: HNil
    classAccessors[MyEndClass]() // (id,Int) :: (after,(k,Int) :: (b,String) :: (c,String) :: (d,String) :: HNil) :: HNil
    

    Deriving nested shapeless lenses using only a type

    Weird behavior trying to convert case classes to heterogeneous lists recursively with Shapeless

    https://github.com/milessabin/shapeless/blob/main/examples/src/main/scala/shapeless/examples/deephlister.scala

    Converting nested case classes to nested Maps using Shapeless

    Automatically convert a case class to an extensible record in shapeless?