Search code examples
scalageneric-programmingshapelesstype-level-computationjvm-bytecode

Extracting type tag with Witness


I'm running some experiments with Witness and now trying to understand how it works. Consider the following example:

import shapeless.syntax.singleton._
import shapeless.labelled.FieldType
import shapeless.Witness

def main(args : Array[String]): Unit = {
  println(getTaggedValue("xxx" ->> 32))
  println(getTaggedValue("yyy" ->> 44))
}

def getTaggedValue[TypeTag, Value](kt: FieldType[TypeTag, Value])
                                (implicit witness: Witness.Aux[TypeTag]): TypeTag = witness.value

As expected, the program prints

xxx
yyy

I tried to understand how these xxx and yyy values were gotten and printed the actual bytecode of the main function:

    62: getstatic     #69                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
    65: ldc           #71                 // String xxx
    67: invokevirtual #75                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
    70: getstatic     #69                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
    73: ldc           #77                 // String yyy
    75: invokevirtual #75                 // Method scala/Predef$.println:(Ljava/lang/Object;)V

As can be seen, all of these Witness-tricks are not presented. The implicit Witness.Aux[TypeTag] came from implicit def apply[T]: Witness.Aux[T] = macro SingletonTypeMacros.materializeImpl[T]

How does it actually work? Is it the compiler who optimized those method invocation? Or this was done via macro?


Solution

  • In the method call getTaggedValue("xxx" ->> 32), the result type is the singleton type "xxx". The Scala 2.12 compiler will in principle inline every expression with a singleton type to the accompanying value (for lack of better terminology, the "value" of type "xxx" is the string "xxx"). So in this case the entire expression getTaggedValue("xxx" ->> 32) and everything it encompasses,like constructing "xxx" ->> 32 and summoning the implicit Witness, will all be erased to the simple value "xxx".

    Singleton types were reworked a bit for Scala 2.13 to make them first-class citizens in the language, and in that process inlining has been made less aggressive. Because if you would add side-effects to your getTaggedValue method you will see that this inlining is not always safe. Which means that when you compile with Scala 2.13 you will see calls such as getTaggedValue and Witness.mkWitness in the bytecode.