0

The intention is to get at run-time some info of particular classes that is available only at compile-time.

My approach was to generate the info at compile time with a macro that expands to a map that contains the info indexed by the runtime class name. Something like this:

object macros {
    def subClassesOf[T]: Map[String, Info] = macro subClassesOfImpl[T];

    def subClassesOfImpl[T: ctx.WeakTypeTag](ctx: blackbox.Context): ctx.Expr[Map[String, Info]] = {
        import ctx.universe._

        val classSymbol = ctx.weakTypeTag[T].tpe.typeSymbol.asClass
        val addEntry_codeLines: List[Tree] =
            for {baseClassSymbol <- classSymbol.knownDirectSubclasses.toList} yield {
                val key = baseClassSymbol.asType.toType.erasure.typeSymbol.fullName
                q"""builder.addOne($key -> new Info("some info"));"""
            }
        q"""
            val builder = Map.newBuilder[String, Info];
            {..$addEntry_codeLines}
            builder.result();""";
        ctx.Expr[Map[String, Info]](body_code);
    }
}

Which would we used like this:

object shapes {
    trait Shape;
    case class Box(h: Int, w: Int);
    case class Sphere(r: Int);
}

val infoMap = subclassesOf[shapes.Shape];
val box = Box(3, 7);
val infoOfBox = infoMap.get(box.getClass.getName)

The problem is that the names of the erased classes given by that macro are slightly different from the ones obtained at runtime by the someInstance.getClass.getName method. The first uses dots to separate container from members, and the second uses dollars.

scala> infoMap.mkString("\n")
val res7: String =
shapes.Box -> Info(some info)
shapes.Sphere -> Info(some info)

scala> box.getClass.getName
val res8: String = shapes$Box

How is the correct way to obtain at compile time the name that a class will have at runtime?

Readren
  • 994
  • 1
  • 10
  • 18
  • What's the goal? Looks like X/Y – cchantep Oct 21 '20 at 21:49
  • @cchantep A scala library that provides the user the ability to automatically convert data from json to custom algebraic data types directly without any intermediate representations. The project is here https://github.com/readren/json-facil – Readren Oct 21 '20 at 22:01
  • Doesn't `circe` 's Shapeless derivation already provide that functionality? or am I missing something? – sinanspd Oct 21 '20 at 22:52
  • Or quite any existing json lib – cchantep Oct 21 '20 at 23:02
  • 1
    @sinanspd For an application like this I think speed efficiency is a concern, and shapeless is quite slow because it creates and drops too many objects. One of the goals of the converters of my library is to minimize garbage creation. That's why the intermediate representation is avoided. – Readren Oct 22 '20 at 00:58

2 Answers2

4

Vice versa, at runtime, having Java name of a class (with dollars) you can obtain Scala name (with dots).

box.getClass.getName 
// com.example.App$shapes$Box

import scala.reflect.runtime
val runtimeMirror = runtime.currentMirror

runtimeMirror.classSymbol(box.getClass).fullName // com.example.App.shapes.Box

This can be done even with replace

val nameWithDot = box.getClass.getName.replace('$', '.')
if (nameWithDot.endsWith(".")) nameWithDot.init else nameWithDot 
// com.example.App.shapes.Box

Anyway, at compile time you can try

def javaName[T]: String = macro javaNameImpl[T]

def javaNameImpl[T: ctx.WeakTypeTag](ctx: blackbox.Context): ctx.Expr[String] = {
  import ctx.universe._
  val symbol = weakTypeOf[T].typeSymbol
  val owners = Seq.unfold(symbol)(symb => 
    if (symb != ctx.mirror.RootClass) Some((symb, symb.owner)) else None
  )
  val nameWithDollar = owners.foldRight("")((symb, str) => {
    val sep = if (symb.isPackage) "." else "$"
    s"$str${symb.name}$sep"
  })
  val name = if (symbol.isModuleClass) nameWithDollar else nameWithDollar.init
  ctx.Expr[String](q"${name: String}")
}

javaName[shapes.Shape] // com.example.App$shapes$Shape

One more option is to use runtime reflection inside a macro. Replace

val key = baseClassSymbol.asType.toType.erasure.typeSymbol.fullName

with

val key = javaName(baseClassSymbol.asType.toType.erasure.typeSymbol.asClass)

where

def subClassesOfImpl[T: ctx.WeakTypeTag](ctx: blackbox.Context): ctx.Expr[Map[String, Info]] = {
  import ctx.universe._

  def javaName(symb: ClassSymbol): String = {
    val rm = scala.reflect.runtime.currentMirror
    rm.runtimeClass(symb.asInstanceOf[scala.reflect.runtime.universe.ClassSymbol]).getName
  }

  ...
}

This works only with classes existing at compile time. So the project should be organized as follows

  • subproject common. Shape, Box, Sphere
  • subproject macros (depends on common). def subClassesOf...
  • subproject core (depends on macros and common). subclassesOf[shapes.Shape]...
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Thank you. I will try the compile time approach. Speed efficiency is a concern so the first approach is discarded and the second creates strings objects at runtime. And I am trying to avoid as most `new`s as possible. – Readren Oct 22 '20 at 01:04
  • 1
    Thinking it again, perhaps the second approach is better if I do the replace in the strings comparator. – Readren Oct 22 '20 at 01:12
  • The second approach (string replace) don't work for module classes. They have a '$' at the end too. – Readren Nov 02 '20 at 04:23
  • 1
    Not so easily. There are other edge cases. One of them is when there are two local classes with the same name. In that case the extra $ is followed by numbers. – Readren Nov 03 '20 at 19:27
  • @Readren Thanks for the edge cases. I guess we shouldn't expect too much from simple work on strings. See one more approach in update of my answer. We can run runtime reflection inside a macro but then we have to organize the project correspondingly. – Dmytro Mitin Nov 04 '20 at 00:57
1

The answer from @Dmytro_Mitin helped me to notice that the scala reflect API does not offer a fast one line method to get the name that a class will have at runtime, and guided me solve my particular problem in another way.

If what you need to know is not the run-time class name itself but only if it matches the name accesible at compile-time, efficiently, then this answer may be useful.

Instead of figuring out what the name will be at runtime, which is apparently not possible before knowing all the classes; just find out if the name we know at compile time corresponds or not to one obtained at runtime. This can be achieved with a string comparator that considers the relevant character and ignores whatever the compiler appends later.

/** Compares two class names based on the length and, if both have the same length, by alphabetic order of the reversed names.
 * If the second name (`b`) ends with a dollar, or a dollar followed by digits, they are removed before the comparison begins. This is necessary because the runtime class name of: module classes have an extra dollar at the end, local classes have a dollar followed by digits, and local object digits surrounded by dollars.
 * Differences between dots and dollars are ignored if the dot is in the first name (`a`) and the dollar in the second name (`b`). This is necessary because the runtime class name of nested classes use a dollar instead of a dot to separate the container from members.
 * The names are considered equal if the fragments after the last dot of the second name (`b`) are equal. */
val productNameComparator: Comparator[String] = { (a, b) =>
    val aLength = a.length;
    var bLength = b.length;
    var bChar: Char = 0;
    var index: Int = 0;

    // Ignore the last segment of `b` if it matches "(\$\d*)+". This is necessary because the runtime class name of: module classes have an extra dollar at the end, local classes have a dollar followed by a number, and local object have a number surrounded by dollars.
    // Optimized versión
    var continue = false;
    do {
        index = bLength - 1;
        continue = false;
        //  find the index of the last non digit character
        while ( {bChar = b.charAt(index); Character.isDigit(bChar)}) {
            index -= 1;
        }
        // if the index of the last non digit character is a dollar, remove it along with the succeeding digits for the comparison.
        if (b.charAt(index) == '$') {
            bLength = index;
            // if something was removed, repeat the process again to support combinations of edge cases. It is not necessary to know all the edge cases if it's known that any dollar or dollar followed by digits at the end are not part of the original class name. So we can remove combinations of them without fear.
            continue = true;
        }
    } while(continue)

    // here starts the comparison
    var diff = aLength - bLength;
    if (diff == 0 && aLength > 0) {
        index = aLength - 1;
        do {
            val aChar = a.charAt(index);
            bChar = b.charAt(index);
            diff = if (aChar == '.' && bChar == '$') {
                0 // Ignore difference between dots and dollars. This assumes that the first name (an) is obtained by the macro, and the second (bn) may be obtained at runtime from the Class object.
            } else {
                aChar - bChar;
            }
            index -= 1;
        } while (diff == 0 && index >= 0 && bChar != '.')
    }
    diff
}

Note that it is designed to compare names of classes that extend the same sealed trait or abstract class. Which means that the names may differ only after the last dot.

Also note that the first argument only supports a compile time name (only dots), while the second supports both, a compile-time or a run-time name.

Readren
  • 994
  • 1
  • 10
  • 18