0

Given 2 Scala case classes

case class Bar(x: Int)
case class Foo(b: Bar, z: Double)

I have a piece of code that prints the types of Foo fields using reflection:

import scala.reflect.runtime.universe._
def f[T: TypeTag] = typeOf[T].members.filter(!_.isMethod)

and I call it like f[Foo] and f[Bar]. Calling the former returns a List[Type] as [Bar, Double].

How can I call f on the first element of the list? Equivalently, how can I print types recursively when Foo has a custom class Bar? Equivalently how can I get from Bar as Type a Bar.type?

Many thanks

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
outlaw
  • 241
  • 4
  • 16
  • `typeOf[Bar]` (unsurprisingly) gives you the `Type` of `Bar`. – Dima Feb 18 '23 at 00:49
  • @Dima So `typeOf[f[Foo].head]` will not work -- I dont see your point : I need something like `f[f[Foo].head]` – outlaw Feb 18 '23 at 00:54
  • 1
    I am not sure where you got `head`. What you need is `foo(t: Type) = t.members.filter(!_.isMethod)`. And then you can have `f[T: Type] = foo(typeOf[T])` if you like. So than you can do `foo[f[Foo].head]` – Dima Feb 18 '23 at 01:18
  • @DmytroMitin thanks -- do macros work also with annotations ? Like can I get also a list of annotations to fields ? – outlaw Feb 18 '23 at 08:33

2 Answers2

3

You don't actually need the type variable T in f. You can define it like this (as Dima suggested in the comments):

def f(t: Type) =
  t.members.filter(!_.isMethod).map(_.typeSignature)

To use this to recursively print a type:

def printTypesRecursive(t: Type, prefix: String = ""): Unit = {
  println(prefix + t)
  f(t).foreach(printTypesRecursive(_, prefix + " "))
}

printTypesRecursive(typeOf[Foo])

Output:

Foo
 Double
 Bar
  Int
Tim Moore
  • 8,958
  • 2
  • 23
  • 34
  • Thanks -- is there a way to convert a `Type` to an actual scala type? Like `Double` to `scala.Double` – outlaw Feb 18 '23 at 08:31
  • To explain more -- like lets say for each `Type` I want the code to behave differently, if `Double` do x, if `Int` do y. Of course I can get the string representation of a `Type` but I would prefer not working with strings but rather with scala types – outlaw Feb 18 '23 at 08:43
  • 1
    You can do type comparisons like `if (t =:= typeOf[Double]) println("DoubleDouble")` – Tim Moore Feb 18 '23 at 11:37
1

Equivalently how can I get from Bar as Type a Bar.type?

Bar.type is the type of companion object

Class companion object vs. case class itself

I need something like f[f[Foo].head]

I guess you have here some confusion between compile-time and runtime

Runtime vs. Compile time

You can call

def f[T: TypeTag] = typeOf[T].members.filter(!_.isMethod)

f[Foo]

//Scope{
//  private[this] val z: <?>;
//  private[this] val b: <?>
//}

if you know type T statically i.e. at compile time (earlier).

You can call

def f_dyn(tpe: Type) = tpe.members.filter(!_.isMethod)

f_dyn(typeOf[Foo])

//Scope{
//  private[this] val z: <?>;
//  private[this] val b: <?>
//}

if you know type tpe dynamically i.e. at runtime (later).

You can express f via f_dyn

def f[T: TypeTag] = f_dyn(typeOf[T])

def f_dyn(tpe: Type) = tpe.members.filter(!_.isMethod)

If you want to iterate the method (apply it recursively) then it should return something like it accepts, i.e. now this is types rather than symbols, so you need to add somewhere something like .typeSignature, .asMethod.returnType, .asType.toType. Also maybe now you're more interested in .decls rather than .members since you are not looking for inherited members. Also .decls returns field symbols in correct order on contrary to .members. Finally let it be better List[...] rather than raw Scope (.toList)

def f[T: TypeTag]: List[Type] = f_dyn(typeOf[T])

def f_dyn(tpe: Type): List[Type] =
  tpe.decls.filter(!_.isMethod).map(_.typeSignature).toList

f_dyn(f[Foo].head)             // List(Int)
f_dyn(f_dyn(typeOf[Foo]).head) // List(Int)

You can iterate f_dyn

f_dyn(typeOf[Foo])                              // List(Bar, Double)
f_dyn(typeOf[Foo]).map(f_dyn)                   // List(List(Int), List())
f_dyn(typeOf[Foo]).map(f_dyn).map(_.map(f_dyn)) // List(List(List()), List())

If you really want to iterate f rather than f_dyn then the complication is that you can call f[T] for the second time only on a statically known type T but you have the type that is the result of the first call only at runtime, you don't have it at compile time. In principle you can use runtime compilation (creating new compile time inside runtime) although this can work slower than ordinary reflection and doesn't seem needed now

import scala.reflect.runtime.{currentMirror => rm}
import scala.tools.reflect.ToolBox // libraryDependencies += scalaOrganization.value % "scala-compiler" % scalaVersion.value
val tb = rm.mkToolBox()

// suppose f is defined in object App 
tb.eval(q"App.f[${f[Foo].head}]")  // List(Int)

tb.eval(q"""
  import App._
  f[${f[Foo].head}]
""")
// List(Int)

Now all the classes Foo, Bar... are defined at compile time so it would make sense to use compile-time reflection (macros) rather than runtime reflection

Getting Case Class definition which points to another Case Class

import scala.language.experimental.macros
import scala.reflect.macros.blackbox

def f[T]: List[String] = macro Macros.f_impl[T]
def f1[T]: List[List[String]] = macro Macros.f1_impl[T]
def f2[T]: List[List[List[String]]] = macro Macros.f2_impl[T]

class Macros(val c: blackbox.Context) {
  import c.universe._

  def f_dyn(tpe: Type): List[Type] =
    tpe.decls.filter(!_.isMethod).map(_.typeSignature).toList

  val ListObj = q"_root_.scala.List"
  val ListT   = tq"_root_.scala.List"
  val StringT = tq"_root_.scala.Predef.String"

  def f_impl[T: WeakTypeTag]: Tree = {
    val types: List[Type] = f_dyn(weakTypeOf[T])
    val typeStrings: List[String] = types.map(_.toString)
    q"$ListObj.apply[$StringT](..$typeStrings)"
  }

  def f1_impl[T: WeakTypeTag]: Tree = {
    val types: List[List[Type]] = f_dyn(weakTypeOf[T]).map(f_dyn)
    val typeStrings: List[List[String]] = types.map(_.map(_.toString))
    q"$ListObj.apply[$ListT[$StringT]](..$typeStrings)"
  }

  def f2_impl[T: WeakTypeTag]: Tree = {
    val types: List[List[List[Type]]] =
      f_dyn(weakTypeOf[T]).map(f_dyn).map(_.map(f_dyn))
    val typeStrings: List[List[List[String]]] = types.map(_.map(_.map(_.toString)))
    q"$ListObj.apply[$ListT[$ListT[$StringT]]](..$typeStrings)"
  }
}
// in a different subproject

f[Foo]
//scalac: _root_.scala.List.apply[_root_.scala.Predef.String]("Bar", "Double")

f1[Foo]
//scalac: _root_.scala.List.apply[_root_.scala.List[_root_.scala.Predef.String]](scala.collection.immutable.List("Int"), scala.collection.immutable.List())

f2[Foo]
//scalac: _root_.scala.List.apply[_root_.scala.List[_root_.scala.List[_root_.scala.Predef.String]]](scala.collection.immutable.List(scala.collection.immutable.List()), scala.collection.immutable.List())

The runtime of macros (when they are expanded) is the compile time of main code.

Do macros support annotations too? like can I access my case class annotations with macros? with runtime reflection , i would do symbolOf[Foo].asClass.annotations

Yes, surely.

def foo[T]: Unit = macro fooImpl[T]
def fooImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Tree = {
  import c.universe._
  println(symbolOf[T].asClass.annotations)
  q"()"
}
class myAnnot extends StaticAnnotation

@myAnnot
case class Foo(b: Bar, z: Double)

symbolOf[Foo].asClass.annotations // at runtime: List(myAnnot)

foo[Foo]
// at compile time with scalacOptions += "-Ymacro-debug-lite":
// scalac: List(myAnnot)

One more option to perform compile-time calculations is to use one of libraries encapsulating work with macros e.g. Shapeless

// libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.10"
import shapeless.{::, DepFn0, DepFn1, HList, HNil, Generic, Poly0, Poly1, Typeable, poly}

trait DeepGeneric[T <: Product] {
  type Repr <: HList
  def to(t: T): Repr
  def from(r: Repr): T
}

object DeepGeneric {
  type Aux[T <: Product, Repr0 <: HList] = DeepGeneric[T] {type Repr = Repr0}
  def instance[T <: Product, Repr0 <: HList](f: T => Repr0, g: Repr0 => T): Aux[T, Repr0] = new DeepGeneric[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
    generic: Generic.Aux[A, L],
    hListDeepGeneric: HListDeepGeneric.Aux[L, L1]
  ): Aux[A, L1] = instance(a => hListDeepGeneric.to(generic.to(a)), l1 => generic.from(hListDeepGeneric.from(l1)))
}

trait HListDeepGeneric[T <: HList] {
  type Repr <: HList
  def to(t: T): Repr
  def from(r: Repr): T
}

trait LowPriorityHListDeepGeneric {
  type Aux[T <: HList, Repr0 <: HList] = HListDeepGeneric[T] {type Repr = Repr0}
  def instance[T <: HList, Repr0 <: HList](f: T => Repr0, g: Repr0 => T): Aux[T, Repr0] = new HListDeepGeneric[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_hListDeepGen <: HList](implicit
    tailHListDeepGeneric: HListDeepGeneric.Aux[T, T_hListDeepGen]
  ): Aux[H :: T, H :: T_hListDeepGen] = instance({
    case h :: t => h :: tailHListDeepGeneric.to(t)
  }, {
    case h :: t => h :: tailHListDeepGeneric.from(t)
  })
}

object HListDeepGeneric extends LowPriorityHListDeepGeneric {
  implicit val hNil: Aux[HNil, HNil] = instance(identity, identity)

  implicit def headCaseClass[H <: Product, T <: HList, H_deepGen <: HList, T_hListDeepGen <: HList](implicit
    headDeepGeneric: DeepGeneric.Aux[H, H_deepGen],
    tailHListDeepGeneric: HListDeepGeneric.Aux[T, T_hListDeepGen]
  ): Aux[H :: T, H_deepGen :: T_hListDeepGen] = instance({
    case h :: t => headDeepGeneric.to(h) :: tailHListDeepGeneric.to(t)
  }, {
    case h :: t => headDeepGeneric.from(h) :: tailHListDeepGeneric.from(t)
  })
}


trait DeepMapper[P <: Poly1, In <: HList] extends DepFn1[In] {
  type Out <: HList
}

trait LowPriorityDeepMapper {
  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, H <: HList, H_deepMap <: HList, T <: HList](implicit
//   headDeepMapper: DeepMapper.Aux[P, H, H_deepMap],
//   headCase: poly.Case1[P, H_deepMap], // apply poly one more time
//   tailDeepMapper: DeepMapper[P, T]
// ): Aux[P, H :: T, headCase.Result :: tailDeepMapper.Out] =
//   instance(l => headCase(headDeepMapper(l.head)) :: tailDeepMapper(l.tail))

  implicit def headHList[P <: Poly1, H <: HList, T <: HList](implicit
    headDeepMapper: DeepMapper[P, H], // don't apply poly one more time
    tailDeepMapper: DeepMapper[P, T]
  ): Aux[P, H :: T, headDeepMapper.Out :: tailDeepMapper.Out] =
    instance(l => 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, H <: HList, T <: HList](implicit
    headDeepFillWith: DeepFillWith[P, H],
    tailDeepFillWith: DeepFillWith[P, T]
  ): DeepFillWith[P, H :: T] =
    instance(headDeepFillWith() :: tailDeepFillWith())
}

// needed if DeepMapper "applies poly one more time",
// e.g. for field NAMES and types (via DeepLabelledGeneric), not just types (via DeepGeneric)
//  trait LowPriorityTypeablePoly extends Poly1 {
//    implicit def notHListCase[V](implicit typeable: Typeable[V]): Case.Aux[V, String] =
//      at(_ => typeable.describe)
//  }
//
//  object typeablePoly extends LowPriorityTypeablePoly {
//    implicit def hListCase[V <: HList]: Case.Aux[V, V] = at(identity)
//  }

object typeablePoly extends Poly1 {
  implicit def cse[A](implicit typeable: Typeable[A]): Case.Aux[A, String] =
    at(_ => typeable.describe)
}

object nullPoly extends Poly0 {
  implicit def cse[A]: Case0[A] = at(null.asInstanceOf[A])
}

def classFieldTypes[T <: Product] = new PartiallyApplied[T]

class PartiallyApplied[T <: Product] {
  def apply[L <: HList]()(implicit
    deepGeneric: DeepGeneric.Aux[T, L],
    deepFillWith: DeepFillWith[nullPoly.type, L],
    deepMapper: DeepMapper[typeablePoly.type, L],
  ): deepMapper.Out = deepMapper(deepFillWith())
}
classFieldTypes[Bar]() // Int :: HNil
classFieldTypes[Foo]() // (Int :: HNil) :: Double :: HNil

Generic/LabelledGeneric/DeepGeneric, Mapper/DeepMapper, FillWith/DeepFillWith, Typeable are type classes.

lets say for each Type I want the code to behave differently, if Double do x, if Int do y.

You can use types comparisons t =:= typeOf[Double], t <:< typeOf[Double] if you use runtime/compile-time reflection or you can keep using type classes and polymorphic functions

trait MyTypeclass[T] {
  def apply(): Unit
}
object MyTypeclass {
  implicit val double: MyTypeclass[Double] = () => println("do x")
  implicit val int: MyTypeclass[Int] = () => println("do y")

  implicit def caseClass[T <: Product, L <: HList](implicit
    deepGeneric: DeepGeneric.Aux[T, L],
    deepFillWith: DeepFillWith[nullPoly.type, L],
    deepMapper: DeepMapper[myPoly.type, L]
  ): MyTypeclass[T] = () => deepMapper(deepFillWith())
}

object myPoly extends Poly1 {
  implicit def cse[T: MyTypeclass]: Case.Aux[T, Unit] = at(_ => foo)
}

def foo[T](implicit tc: MyTypeclass[T]): Unit = tc()

foo[Int]
// do y
foo[Double]
// do x
foo[Foo]
// do y
// do x
foo[Bar]
// do y

Shapeless is also capable of handling annotations

import shapeless.Annotation

implicitly[Annotation[myAnnot, Foo]].apply() // myAnnot@1a3869f4
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Thanks for the comment - yes indeed I had the compile time and runtime confusion but now its clear. I am considering using macros -- it is just that I already started using runtime reflection. Do macros support annotations too? like can I access my case class annotations with macros? with runtime reflection , i would do `symbolOf[Foo].asClass.annotations` – outlaw Feb 18 '23 at 09:58
  • @outlaw Yes. See example in the update – Dmytro Mitin Feb 18 '23 at 10:48
  • @outlaw See the update with Shapeless and how to manage different types (`Double`, `Int` etc.) – Dmytro Mitin Feb 18 '23 at 13:09
  • what is the shapeless based code much more verbose than runtime reflection or macros? I was a bit surprised as I expected it to be more concise – outlaw Feb 19 '23 at 07:41
  • @outlaw Well, firstly, recursion on type classes is more verbose than just a recursive method, a type class is more verbose than a method. Type classes is how calculations in Scala (or Haskell etc.) are moved from runtime to compile time. Secondly, in our case, in Shapeless approach I just mixed infrastructural code with the code for the current task. `DeepGeneric`/`HListDeepGeneric`, `DeepMapper`, `DeepFillWith` is infrastructure. – Dmytro Mitin Feb 19 '23 at 09:29
  • @outlaw These type classes are usefull not only for current task. For some reason, on contrary to standard `Generic`/`LabelledGeneric`, `Mapper`, `FillWith` existing in Shapeless, their deep version don't although they should. Maybe because there can be different flavors of how they should work deeply. – Dmytro Mitin Feb 19 '23 at 09:29
  • @outlaw For example `DeepMapper` can "apply poly one more time" or not, `DeepGeneric`, `DeepMapper`, `DeepFillWith` can work on raw HLists `H :: T` (as in this question) or on records `FieldType[K, V] :: T` (as in https://stackoverflow.com/questions/75286557) or on their mixure. So the code for current task is just `classFieldTypes` and it's not so verbose. – Dmytro Mitin Feb 19 '23 at 09:30
  • @outlaw You can compare with https://stackoverflow.com/questions/66672681 https://stackoverflow.com/questions/63298926 https://stackoverflow.com/questions/55504430 where the type classes do not need to work deeply (with nested case classes or nested HLists or nested records), so standard type classes are enough, and there the code is not so verbose – Dmytro Mitin Feb 19 '23 at 09:46
  • @outlaw With macros or runtime reflection you can see that I didn't prepare any infrastructure. I just applied your method one, two or three times. If I started to apply it N times, started to combine the results, started to calculate the return type (`List[List[...List[Type]]]`), then the code would be not so short either. – Dmytro Mitin Feb 19 '23 at 09:58