scalaloggingreflectionscala-macros

Does anybody know, how to properly print case class type hierarchy without creating one?


I faced a problem. I'd like to make a printer for any type in scala. for example i have a case class

  class AAA(i: Int, s: String, o: Option[Int], bbb: BBB)
  class BBB(l: List[Int])
def explainType[T]: String

and i need a function which can take a type of this class and return a string like

AAA(i: Int, s: String, o: Option(if possible with inner type), bbb:(l: List(if possible with inner type))

I don't really care about format, it needs to be self-explanatory. If it's possible to print it as json, i'd like that. If you know any existing library for that kind of thing, please let me know.

Thanks in advance.


Solution

  • If your classes are case classes you can implement the type class Explain etc. with Shapeless and Circe

    // I'm using Shapeless but still need this macro :)
    
    import scala.language.experimental.macros
    import scala.reflect.macros.whitebox // libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value
    
    trait ToName[A] {
      type Out <: String with Singleton
    }
    object ToName {
      type Aux[A, Out0 <: String with Singleton] = ToName[A] { type Out = Out0 }
    
      implicit def mkToName[A, Out <: String with Singleton]: Aux[A, Out] = macro mkToNameImpl[A]
    
      def mkToNameImpl[A: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
        import c.universe._
        val A = weakTypeOf[A]
        val toNameTpe = weakTypeOf[ToName[A]]
        q"""
          new $toNameTpe {
            type Out = ${A.typeSymbol.name.toString}
          }
        """
      }
    }
    
    // in a different subproject
    
    import io.circe.{Json, JsonObject} // libraryDependencies += "io.circe" %% "circe-core" % "0.14.5"
    import shapeless.{::, HList, HNil, LabelledGeneric, Typeable, Witness} // libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.10"
    import shapeless.labelled.{FieldType, field}
    import shapeless.tag.@@
    
    trait ToNameSymbol[A] {
      type Out <: Symbol
    }
    object ToNameSymbol {
      type Aux[A, Out0 <: Symbol] = ToNameSymbol[A] {type Out = Out0}
    
      implicit def mkToNameSymbol[A](implicit toName: ToName[A]): Aux[A, Symbol @@ toName.Out] = null
    }
    
    trait LabelledGenericWithName[T <: Product] {
      type Repr <: FieldType[_, _ <: HList]
      def to(t: T): Repr
      def from(r: Repr): T
    }
    object LabelledGenericWithName {
      type Aux[T <: Product, Repr0 <: FieldType[_, _ <: HList]] = LabelledGenericWithName[T] {type Repr = Repr0}
      def instance[T <: Product, Repr0 <: FieldType[_, _ <: HList]](f: T => Repr0, g: Repr0 => T): Aux[T, Repr0] = new LabelledGenericWithName[T] {
        override type Repr = Repr0
        override def to(t: T): Repr = f(t)
        override def from(r: Repr): T = g(r)
      }
    
      implicit def mkLabelledGenericWithName[T <: Product, L <: HList](implicit
        labelledGeneric: LabelledGeneric.Aux[T, L],
        toNameSymbol: ToNameSymbol[T]
      ): Aux[T, FieldType[toNameSymbol.Out, L]] = instance(
        t => field[toNameSymbol.Out](labelledGeneric.to(t)),
        t => labelledGeneric.from(t)
      )
    }
    
    trait DeepLabelledGeneric[T <: Product] {
      type Repr <: FieldType[_, _ <: HList]
      def to(t: T): Repr
      def from(r: Repr): T
    }
    object DeepLabelledGeneric {
      type Aux[T <: Product, Repr0 <: FieldType[_, _ <: HList]] = DeepLabelledGeneric[T] {type Repr = Repr0}
      def instance[T <: Product, Repr0 <: FieldType[_, _ <: 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 mkDeepLabelledGeneric[A <: Product, K <: Symbol, L <: HList, L1 <: HList](implicit
        labelledGenericWithName: LabelledGenericWithName.Aux[A, FieldType[K, L]],
        hListDeepLabelledGeneric: HListDeepLabelledGeneric.Aux[L, L1]
      ): Aux[A, FieldType[K, L1]] =
        instance(
          a => field[K](hListDeepLabelledGeneric.to(labelledGenericWithName.to(a))),
          l1 => labelledGenericWithName.from(field[K](hListDeepLabelledGeneric.from(l1)) : FieldType[K, L])
        )
    }
    
    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 <: FieldType[_, _ <: 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 Explain[T <: Product] {
      def apply(): JsonObject
    }
    object Explain {
      implicit def mkExplain[T <: Product, K <: Symbol, L <: HList](implicit
        deepLabelledGeneric: DeepLabelledGeneric.Aux[T, FieldType[K, L]],
        hListExplain: HListExplain[L],
        witness: Witness.Aux[K]
      ): Explain[T] = () => JsonObject(witness.value.name -> Json.fromJsonObject(hListExplain()))
    }
    
    trait HListExplain[L <: HList] {
      def apply(): JsonObject
    }
    object HListExplain {
      implicit val hNil: HListExplain[HNil] = () => JsonObject()
    
      implicit def headNotHList[K <: Symbol, H, T <: HList](implicit
        tailHListExplain: HListExplain[T],
        witness: Witness.Aux[K],
        typeable: Typeable[H]
      ): HListExplain[FieldType[K, H] :: T] =
        () => (witness.value.name -> Json.fromString(typeable.describe)) +: tailHListExplain()
    
      implicit def headHList[K <: Symbol, K1 <: Symbol, H <: HList, T <: HList](implicit
        headHListExplain: HListExplain[H],
        tailHListExplain: HListExplain[T],
        witness: Witness.Aux[K],
        witness1: Witness.Aux[K1],
      ): HListExplain[FieldType[K, FieldType[K1, H]] :: T] =
        () => (witness.value.name -> Json.obj(witness1.value.name -> Json.fromJsonObject(headHListExplain()))) +: tailHListExplain()
    }
    
    def explainType[T <: Product](implicit explain: Explain[T]): Json = Json.fromJsonObject(explain())
    
    case class AAA(i: Int, s: String, o: Option[Int], bbb: BBB)
    case class BBB(l: List[Int])
    
    explainType[AAA]
    //{
    //  "AAA" : {
    //    "i" : "Int",
    //    "s" : "String",
    //    "o" : "Option[Int]",
    //    "bbb" : {
    //      "BBB" : {
    //        "l" : "List[Int]"
    //      }
    //    }
    //  }
    //}
    

    Shapeless - How to derive LabelledGeneric for Coproduct (ToName)

    Deriving nested shapeless lenses using only a type (DeepGeneric)

    Getting Case Class definition which points to another Case Class (DeepLabelledGeneric)

    Scala case classes and recursive reflection (DeepGeneric)

    Get case class field's name and type with shapeless

    How to get field names and field types from a Generic Type in Scala?

    How to get the name of a class as a string literal at compile time using shapeless?


    If the classes are not necessarily case classes you can implement the type class Explain etc. with macros. Namely, you can replace standard shapeless.Generic, LabelledGeneric, DefaultSymbolicLabelling with the implementations below working not only with case classes. Also, comparing with the above implementation, I'm replacing the upper bound <: Product with a type class (context bound) IsCaseClassLike. Otherwise, if I just remove <: Product and do not add any constraint then the compiler starts to look for generic representation of standard classes like Int, String etc.

    import shapeless.{Annotation, DepFn0, HList, Refute}
    import scala.annotation.StaticAnnotation
    import scala.language.experimental.macros
    import scala.reflect.macros.whitebox
    
    trait ToName[A] {
      type Out <: String with Singleton
    }
    object ToName {
      type Aux[A, Out0 <: String with Singleton] = ToName[A] { type Out = Out0 }
    
      implicit def mkToName[A, Out <: String with Singleton]: Aux[A, Out] = macro mkToNameImpl[A]
    
      def mkToNameImpl[A: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
        import c.universe._
        val A = weakTypeOf[A]
        val toNameTpe = weakTypeOf[ToName[A]]
        q"""
          new $toNameTpe {
            type Out = ${A.typeSymbol.name.toString}
          }
        """
      }
    }
    
    // class data extends StaticAnnotation
    
    trait IsCaseClassLike[T]
    object IsCaseClassLike {
       // nothing should be converted to HList except classes annotated with @data
    // implicit def mkIsCaseClassLike[T](implicit ev: Annotation[data, T]): IsCaseClassLike[T] = null
    
      // everything should be converted to HList except standard classes
      implicit def mkIsCaseClassLike[T](implicit ev: Refute[IsStandard[T]]): IsCaseClassLike[T] = null
    }
    
    trait IsStandard[T]
    object IsStandard {
      implicit def mkIsStandard[T]: IsStandard[T] = macro mkIsStandardImpl[T]
    
      def mkIsStandardImpl[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
        import c.universe._
        val symbol = symbolOf[T]
        if (Seq("scala.", "java.", "shapeless.").exists(symbol.fullName.startsWith)) q"null"
        else c.abort(c.enclosingPosition, s"$symbol is not standard class")
      }
    //    implicit val int: IsStandard[Int] = null
    //    implicit val str: IsStandard[String] = null
    //    implicit def opt[A]: IsStandard[Option[A]] = null
    //    implicit def list[A]: IsStandard[List[A]] = null
    //    implicit def hlist[L <: HList]: IsStandard[L] = null
    }
    
    trait Generic[T] {
      type Repr <: HList
      def to(t: T): Repr
      def from(r: Repr): T
    }
    object Generic {
      type Aux[T, Repr0 <: HList] = Generic[T] {type Repr = Repr0}
      def instance[T, R <: HList](f: T => R, g: R => T): Aux[T, R] = new Generic[T] {
        override type Repr = R
        override def to(t: T): Repr = f(t)
        override def from(r: Repr): T = g(r)
      }
    
      implicit def mkGeneric[T, R <: HList](implicit
        ev: IsCaseClassLike[T]
      ): Aux[T, R] = macro mkGenericImpl[T]
    
      def mkGenericImpl[T : c.WeakTypeTag](c: whitebox.Context)(ev: c.Tree): c.Tree = {
        import c.universe._
    
        val T = weakTypeOf[T]
        val genTpe = weakTypeOf[Generic[T]]
        val fieldTypes = T.decls.filter(s => s.isTerm && !s.isMethod).map(_.typeSignature)
        val range = 0 until fieldTypes.size
        val names = range.map(i => q"${TermName(s"arg$i")}")
        val namePatterns = range.map(i => pq"${TermName(s"arg$i")}")
        val sh = q"_root_.shapeless"
        val (reprTpe, repr) =
          fieldTypes.zip(names)
            .foldRight[(Tree, Tree)]((tq"$sh.HNil", q"$sh.HNil")) { case ((tpe, name), (accTpe, acc)) =>
              val accTpe1 = tq"$sh.::[$tpe, $accTpe]"
              (accTpe1, q"new $accTpe1($name, $acc)")
            }
        val reprPattern = namePatterns.foldRight[Tree](pq"$sh.HNil")((name, acc) => pq"$sh.::($name, $acc)")
        val reprCase = cq"$reprPattern => new $T(..$names)"
    
        val to = 
          if (T.companion != NoType && T.companion.decl(TermName("unapply")) != NoSymbol) {
            val classCase = cq"${T.typeSymbol.companion}(..$namePatterns) => $repr"
            q"t match { case $classCase }"
          } else q"_root_.scala.Predef.???"
    
        val from = q"r match { case $reprCase }"
    
        q"""
          new $genTpe {
            override type Repr = $reprTpe
            override def to(t: $T): Repr   = $to
            override def from(r: Repr): $T = $from
          }
        """
      }
    }
    
    trait DefaultSymbolicLabelling[T] extends DepFn0 {
      type Out <: HList
    }
    object DefaultSymbolicLabelling {
      type Aux[T, Out0 <: HList] = DefaultSymbolicLabelling[T] {type Out = Out0}
    
      implicit def mkDefaultSymbolicLabelling[T, Out <: HList](implicit
        ev: IsCaseClassLike[T]
      ): Aux[T, Out] = macro mkDefaultSymbolicLabellingImpl[T]
    
      def mkDefaultSymbolicLabellingImpl[T: c.WeakTypeTag](c: whitebox.Context)(ev: c.Tree): c.Tree = {
        import c.universe._
        val T = weakTypeOf[T]
        val dslTpe = weakTypeOf[DefaultSymbolicLabelling[T]]
        val fieldNames = T.decls.filter(s => s.isTerm && !s.isMethod).map(_.name.toString.stripSuffix(" "))
        val sh = q"_root_.shapeless"
        val Sym = q"_root_.scala.Symbol"
        val SymT = tq"_root_.scala.Symbol"
        val (outTpe, out) =
          fieldNames.foldRight[(Tree, Tree)]((tq"$sh.HNil", q"$sh.HNil")) { case (name, (accTpe, acc)) =>
            val accTpe1 = tq"$sh.::[$sh.tag.@@[$SymT, $name], $accTpe]"
    
            (
              accTpe1,
              q"""
                new $accTpe1(
                  $sh.tag.apply[$name].apply[$SymT]($Sym.apply($name)),
                  $acc
                )
              """
            )
          }
    
        q"""
          new $dslTpe {
            override type Out = $outTpe
            override def apply(): Out = $out
          }
        """
      }
    }
    
    import io.circe.{Json, JsonObject}
    import shapeless.{::, HList, HNil, Typeable, Witness, Unpack2}
    import shapeless.labelled.{FieldType, KeyTag, field}
    import shapeless.ops.hlist.ZipWithKeys
    import shapeless.tag.@@
    
    trait LabelledGeneric[T] {
      type Repr <: HList
      def to(t: T): Repr
      def from(r: Repr): T
    }
    object LabelledGeneric {
      type Aux[T, Repr0 <: HList] = LabelledGeneric[T] {type Repr = Repr0}
      def instance[T, Repr0 <: HList](f: T => Repr0, g: Repr0 => T): Aux[T, Repr0] = new LabelledGeneric[T] {
        override type Repr = Repr0
        override def to(t: T): Repr = f(t)
        override def from(r: Repr): T = g(r)
      }
    
      implicit def mkLabelledGeneric[T, K <: HList, V <: HList, R <: HList](implicit
        ev0: IsCaseClassLike[T],
        lab: DefaultSymbolicLabelling.Aux[T, K],
        gen: Generic.Aux[T, V],
        zip: ZipWithKeys.Aux[K, V, R],
        ev: R <:< V
      ): Aux[T, R] = instance(t => zip(gen.to(t)), gen.from(_))
    }
    
    trait ToNameSymbol[A] {
      type Out <: Symbol
    }
    object ToNameSymbol {
      type Aux[A, Out0 <: Symbol] = ToNameSymbol[A] {type Out = Out0}
    
      implicit def mkToNameSymbol[A](implicit
        toName: ToName[A]
      ): Aux[A, Symbol @@ toName.Out] = null
    }
    
    trait LabelledGenericWithName[T] {
      type Repr <: FieldType[_, _ <: HList]
      def to(t: T): Repr
      def from(r: Repr): T
    }
    object LabelledGenericWithName {
      type Aux[T, Repr0 <: FieldType[_, _ <: HList]] = LabelledGenericWithName[T] {type Repr = Repr0}
      def instance[T, Repr0 <: FieldType[_, _ <: HList]](f: T => Repr0, g: Repr0 => T): Aux[T, Repr0] = new LabelledGenericWithName[T] {
        override type Repr = Repr0
        override def to(t: T): Repr = f(t)
        override def from(r: Repr): T = g(r)
      }
    
      implicit def mkLabelledGenericWithName[T, L <: HList](implicit
        ev: IsCaseClassLike[T],
        labelledGeneric: LabelledGeneric.Aux[T, L],
        toNameSymbol: ToNameSymbol[T]
      ): Aux[T, FieldType[toNameSymbol.Out, L]] =
        instance(
          t => field[toNameSymbol.Out](labelledGeneric.to(t)),
          t => labelledGeneric.from(t)
        )
    }
    
    trait DeepLabelledGeneric[T] {
      type Repr <: FieldType[_, _ <: HList]
      def to(t: T): Repr
      def from(r: Repr): T
    }
    object DeepLabelledGeneric {
      type Aux[T, Repr0 <: FieldType[_, _ <: HList]] = DeepLabelledGeneric[T] {type Repr = Repr0}
      def instance[T, Repr0 <: FieldType[_, _ <: 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 mkDeepLabelledGeneric[A, K <: Symbol, L <: HList, L1 <: HList](implicit
        ev: IsCaseClassLike[A],
        labelledGenericWithName: LabelledGenericWithName.Aux[A, FieldType[K, L]],
        hListDeepLabelledGeneric: HListDeepLabelledGeneric.Aux[L, L1]
      ): Aux[A, FieldType[K, L1]] =
        instance(
          a => field[K](hListDeepLabelledGeneric.to(labelledGenericWithName.to(a))),
          l1 => labelledGenericWithName.from(field[K](hListDeepLabelledGeneric.from(l1)) : FieldType[K, L])
        )
    }
    
    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, T <: HList, H_deepLGen <: FieldType[_, _ <: HList], T_hListDeepLGen <: HList](implicit
        ev: IsCaseClassLike[H],
        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 Explain[T] {
      def apply(): JsonObject
    }
    object Explain {
      implicit def mkExplain[T, /*FT <: FieldType[_, _ <: HList],*/ K <: Symbol, L <: HList](implicit
        ev: IsCaseClassLike[T],
          // I was afraid that .Aux[T, FieldType[K, L]] will be over-constrained implicit, see (*)
        deepLabelledGeneric: DeepLabelledGeneric.Aux[T, FieldType[K, L]], //DeepLabelledGeneric.Aux[T, FT],
        // ev1: Unpack2[FT, FieldType, K, L], // FT <:< FieldType[K, L], // FT =:= FieldType[K, L], // FT <:< KeyTag[K, L],
        hListExplain: HListExplain[L],
        witness: Witness.Aux[K]
      ): Explain[T] = () => JsonObject(witness.value.name -> Json.fromJsonObject(hListExplain()))
    }
    
    trait HListExplain[L <: HList] {
      def apply(): JsonObject
    }
    object HListExplain {
      implicit val hNil: HListExplain[HNil] = () => JsonObject()
    
      implicit def headNotHList[K <: Symbol, H, T <: HList](implicit
        tailHListExplain: HListExplain[T],
        witness: Witness.Aux[K],
        typeable: Typeable[H]
      ): HListExplain[FieldType[K, H] :: T] =
        () => (witness.value.name -> Json.fromString(typeable.describe)) +: tailHListExplain()
    
      implicit def headHList[K <: Symbol, K1 <: Symbol, H <: HList, T <: HList](implicit
        headHListExplain: HListExplain[H],
        tailHListExplain: HListExplain[T],
        witness: Witness.Aux[K],
        witness1: Witness.Aux[K1],
      ): HListExplain[FieldType[K, FieldType[K1, H]] :: T] =
        () => (witness.value.name -> Json.obj(witness1.value.name -> Json.fromJsonObject(headHListExplain()))) +: tailHListExplain()
    }
    
    def explainType[T](implicit explain: Explain[T]): Json = Json.fromJsonObject(explain())
    
    /*@data*/
    /*case*/ class AAA(val i: Int, val s: String, o: Option[Int], bbb: BBB)
    
    /*@data*/
    /*case*/ class BBB(l: List[Int])
    
    explainType[AAA]
    //{
    //  "AAA" : {
    //    "i" : "Int",
    //    "s" : "String",
    //    "o" : "Option[Int]",
    //    "bbb" : {
    //      "BBB" : {
    //        "l" : "List[Int]"
    //      }
    //    }
    //  }
    //}
    

    (*) Why is this implicit resolution failing? , HList foldLeft with tuple as zero