Java’s reflection mechanism allows programs to retrieve information about a class at run time to make our programs flexible, and the same is true for learning Scala reflection. Scala’s reflection really falls into two categories: runtime reflection and compile-time reflection. This section covers only the common run-time reflections. Compile-time reflection is primarily used for metaprogramming — something I would prefer to do with another flexible and easy to use JVM language, Groovy. See: Groovy AST Tour – Nuggets (juejin. Cn)

Before we understand reflection, we should be familiar with Scala’s concepts of generics, deformations, context demarcation, implicit transformations, and Java generic erasure.

This is the last part of the introduction to Scala’s basic concepts. My next topic in this column will be “How to gracefully implement functional programming in Scala.”

Type erasure: Generics are a mirage

A conceptual review of the Java type erasure mechanism comes from this article: Java type erasure

Before generics, abstractions of the same data type could be summarized using class without causing any ambiguity. After the advent of generics, however, there was a subtle shift: should List

and List

be the same class?

public class Generic {
    public static void main(String[] args) {

        // They are compiled as LinkedList x = new LinkedList();
        LinkedList<Integer> a = new LinkedList<>();
        LinkedList<Double> b = newLinkedList<>(); System.out.println(a.getClass() == b.getClass()); }}Copy the code

Since Java doesn’t deal with higher-order types, when this code is compiled by the compiler, the getClass() method doesn’t tell the difference between variable A and variable B, The so-called

and

are replaced by Object or the upper bound of the generic type at the bottom (you can also guess why Java generics don’t support basic data types).

Scala takes this problem a little more seriously: it introduces the concept of Type here. Type itself encompasses not only classes in the traditional sense, but also differences in Type parameters between classes. In other words, if they are of the same Type, they must belong to the same class, whereas if they are of the same class, they do not necessarily belong to the same Type.

1. Runtime reflection

Scala has two apis for runtime reflection: TypeTag and WeakTypeTag for Type Type, and ClassTag for class (regardless of Type parameters) class. Obviously, the former is more demanding on equality. Prior to version 2.10, Scala also provided the Manifest and ClassManifest apis for runtime reflection, but they are now out of date, so I won’t go into them again.

1.0 Introduction: Some statements

For Scala’s runtime reflection, we first need to import the following dependencies (either in the REPL or.scala file). And scala. Reflect. Runtime. The universe is actually a lazy loading JavaUniverse type instance, there’s statement about it at the scala. Reflect the runtime package found objects.

import scala.reflect.runtime.universe._
Copy the code

TypeTag and WeakTypeTag are path-dependent types prefixed with universe as mentioned in the chapter of reflection at runtime. As they actually are scala. Reflect. API. TypeTags inner classes (here also involves complex inheritance, but the author here ignored). ClassTag is a separate type located elsewhere. That is:

TypeTag => scala.reflect.runtime.universe.TypeTag ==>  scala.reflect.api.TypeTags#TpyeTag
WeakTypeTag => scala.reflect.runtime.universe.WeakTypeTag => scala.reflect.api.TypeTags#WeakTypeTag
ClassTag => scala.reflect.ClassTag
Copy the code

Similarly, methods such as typeOf, weakTypeOf, etc. are actually methods provided by universe instances.

typeOf[T] => scala.reflect.runtime.universe.typeOf[T]
weakTypeOf[T] => scala.reflect.runtime.universe.weakTypeOf[T]
...
Copy the code

If you don’t want to import the entire contents of universe, you can also call them this way (in some cases, I have chosen to do this to avoid accidentally importing implicit transformations, as I did in the Mirror section) :

// ru : runtime
val ru = scala.reflect.runtime.universe
ru.typeOf[T]    // => scala.reflect.runtime.universe.typeOf[T]
ru.TypeTag[T]   // => scala.reflect.runtime.universe.TypeTag[T]
Copy the code

For the sake of the reading experience, I will omit their paths below (because they are too long). Also, much of the code in this article runs in the REPL interactive environment, where you can access the terminal using scala commands from the host.

1.1 TypeTag

TypeTag is a generic method that takes complete information about a type and returns it wrapped in typeTag (uppercase) classes, including their internal type parameters.

scala> val tt = typeTag[List[List[String]]]
tt: reflect.runtime.universe.TypeTag[List[List[String=]]]TypeTag[scala.List[scala.List[String]]]
Copy the code

The full Type of the TypeTag can be extracted from the TypeTag by calling the.tpe method of the TypeTag and returned as Type:

scala> tt.tpe
res0: reflect.runtime.universe.Type = scala.List[scala.List[String]]
Copy the code

Or call the typeOf method directly to get a full Type and return Type:

scala> typeOf[List[List[String]]]
res1: reflect.runtime.universe.Type = scala.List[scala.List[String]]
Copy the code

Now, define a getTypeTag method that accepts an object of any type and gets its full type information. Then I’ll explain why we need a [T: TypeTag] context definition in the next section:

scala> def getTypeTag[T: TypeTag](o: T) :Type = typeOf[T]
getTypeTag: [T](o: T) (implicit evidence$1: reflect.runtime.universe.TypeTag[T])reflect.runtime.universe.Type

scala> val o = List[List[String]]()
o: List[List[String]] = List()

scala> getTypeTag1(o).toString
res2: String = List[scala.List[String]]
Copy the code

Scala stores type information in TypeTag during compilation and carries it to runtime. If we want to know the Type of T, we must implicitly request a TypeTag[T] from the compiler (the TypeT is determined at compile time), and then we can get the Type of T from the typeOf method. The code block above converts implicit parameters to context-specific writing.

Note that when this getTypeTag method is called, its type T must be determinable, and the compiler will report an error if another undefined generic U is passed to it. In this case, weakTypeTag should be used instead.

1.1.1 To achieve more accurate pattern matching

This is a hangover from the previous pattern matching chapter. Due to the JVM type erasure mechanism, Scala programs are helpless for such pattern matching:

def typeOfMap(map : Map[_, _]) :Unit = {
  map match {
    // In fact, maps with Any type of argument are all erased at compile time to Map[Any,Any], so the first branch is always executed.
    case_ :Map[Int.Int] => println("this is a Map[Int,Int]")
    case_ :Map[Int.String] => println("this is a Map[Int,String]")
    case _ => println("not a valid map.")
  }
}

typeOfMap(Map[String.String] ())Copy the code

No matter how elaborate a pattern match is designed, the program can only tell if it is a Map, not the difference between Map[Int,Int] and Map[Int,String]. Type, on the other hand, returns the full Type information containing the generics, which we can use to solve this problem:

def typeOfMap_+[K : TypeTag.V : TypeTag](map: Map[K.V) :Unit = {

    typeOf[K] match {
        case ktp if ktp =:= typeOf[String] => println("key's type is String!")
        case ktp if ktp =:= typeOf[Int] => println("key's type is Int!")
        case _ => println("key is neither String or Int.")
    }

    typeOf[V] match {
        case vtp if vtp =:= typeOf[String] => println("value's type is String!")
        case vtp if vtp =:= typeOf[Int] => println("value's type is Int!")
        case _ => println("value is neither String or Int.")
    }
}

typeOfMap_+(Map[String.String] ())Copy the code

This time, the pattern matching can accurately identify the specific type of Map[_,_].

We used this example to introduce a comparison method between types. Let A and B both belong to Type Type:

  1. A =:= BSaid,ABAre of the same type and contain their type parameters.
  2. A <:< BSaid,ABSubtype of,Or the same type.

For exactly the same type, either <:< or =:=, the result is true. However, the comparison of Type is not as simple as imagined, and the author will discuss two cases below.

1.1.2 TypeEquality in generic classes

Scala’s generics support deformation. We need to discuss further what “parent” and “equal” are for generic classes. Here’s the actual code listing, mainly comparing A and B attributes, and comparing their types by getting Type.

scala> trait B[T] {}
defined trait B

scala> trait A[U] extends B[U] {}
defined trait A

scala> class Father {}
defined class Father

scala> class Son extends Father {}
defined class Son
Copy the code

Notice that T and U are constant. This means that for the B[Father] type, only A[Father] is considered A subtype, and nothing else is considered A subtype. Similarly, any other B[_] is not considered a subtype.

scala> println(typeTag[B[Father]].tpe <:< typeTag[B[Father]].tpe)
true

scala> println(typeTag[B[Father]].tpe =:= typeTag[B[Father]].tpe)
true

scala> println(typeTag[B[Son]].tpe <:< typeTag[B[Father]].tpe)
false

scala> println(typeTag[A[Father]].tpe <:< typeTag[B[Father]].tpe)
true

scala> println(typeTag[A[Son]].tpe <:< typeTag[B[Father]].tpe)
false

Copy the code

If T and U are covariant, this means that for B[Father], either A[Father], A[Son], or B[Son] can be considered subtypes (including itself). For A[Father], A[Son] can also be used as its subtype.

scala> trait B[+T] {}
defined trait B

scala> trait A[+U] extends B[U] {}
defined trait A

scala> class Father
defined class Father

scala> class Son extends Father
defined class Son
Copy the code

Testing our code in the REPL, the reasoning is correct: the deformation of generics affects the determination of Type Type.

scala> println(typeTag[A[Father]].tpe <:< typeTag[B[Father]].tpe)
true

scala> println(typeTag[A[Son]].tpe <:< typeTag[B[Father]].tpe)
true

scala> println(typeTag[B[Son]].tpe <:< typeTag[B[Father]].tpe)
true

scala> println(typeTag[A[Son]].tpe <:< typeTag[A[Father]].tpe)
true
Copy the code

In the case that T and U are contravariant, the result should also be easy to derive, and I do not give it here.

1.1.3 TypeEquality in path-dependent classes

Create a class Outer with a member Inner and create two Inner instances with different paths:

scala> class Outer {|class Inner
     | }
defined class Outer {}

scala> val outer1 = new Outer
outer1: Outer = Outer@68f4865

scala> val outer2 = new Outer
outer2: Outer = Outer@4196c360

scala> val inner1 = new outer1.Inner
inner1: outer1.Inner = Outer$Inner@1e44b638

scala> val inner2 = new outer2.Inner
inner2: outer2.Inner = Outer$Inner@7164ca4c
Copy the code

The command line shows that they are both essentially Outer$Inner classes, so if you compare their classes, the result is true. However, if you compare the two types, the result is false because their “paths” (or prefixes) are different.

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> println(typeOf[outer1.Inner] =:= typeOf[outer2.Inner])
false
Copy the code

1.2 WeakTypeTag

For the getTypeTag method described in the TypeTag section above:

def getTypeTag[T: TypeTag](o: T) :Type = typeOf[T]
Copy the code

If you nested another layer of functions and passed a type argument U to getTypeTag:

def getTypeTag[T: TypeTag](o: T) :Type = typeOf[T]

def foo[U] (x : U)= getTypeTag[U](x)
Copy the code

Error:(xx, xx) No TypeTag available for U The reason is that the Type parameter U passed is also undefined, and TypeTag requires that an explicit Type parameter be passed in order to get a complete Type Type through.tpe.

WeakTypeTag provides a broader limitation, that is, WeakTypeTag[T] allows the type parameter T to be nonspecific (or abstract).

def getTypeTag[T: WeakTypeTag](o: T) :Type = weakTypeOf[T]

def foo[U] (x : U)= getTypeTag[U](x)
Copy the code

The following is the description related to WeakTypeTag provided in the official Scala documentation: Link.

1.3 ClassTag

ClassTag is also a generic method used to get the erased class. The return value is ClassTag. ClassTag is in a location-independent package (see the declaration in the introduction), and before using it for reflection, you should first import the import scala.reflection.classtype.

scala> classTag[List[List[String]]]
res1: scala.reflect.ClassTag[List[List[String]]] = scala.collection.immutable.List
Copy the code

You can retrieve its Class type with.runtimeclass.

scala> res1.runtimeClass
res2: Class[_] = class scala.collection.immutable.List
Copy the code

Similar to typeOf methods, Class[_] can be obtained directly through the classOf generic method.

scala> classOf[List[List[Int]]]
res3: Class[List[List[Int=]]]class scala.collection.immutable.List
Copy the code

Once we get this Class[_], we’ll use methods like getAnnotations, getFields, and getMethods to retrieve information about this Class. These notations are similar to traditional Java reflections, so I won’t highlight them here. The classOf method is defined under Scala.predef, which means you can call the method and get the corresponding Class[_] without manually importing any dependencies.

1.4 Mirror

Reflection is like a mirror — just by putting the example in front of the mirror, you can get the full picture.

You can think of Scala’s reflection process as taking a “mirror” of the object (that is, a “mirror”) and using that mirror to retrieve properties, methods, and so on inside the object. According to the different contents of the reflection, the author sorted out the following levels of Mirror:

Here we define an additional Person type for experimenting with reflection and try to deconstruct it with reflection (note: Companion classes and companion objects should always be defined in a file. To define an associated object and class in the REPL, enter paste mode and press CTRL + D to exit after declaring both definitions.

scala> :paste
// Entering paste mode (ctrl-D to finish)
class Person (val name:String,val age:Int) {
  private def unsafe() :Unit = println("private method.")}object Person {
  def greet() :Unit = println("hello!")}// Exiting paste mode, now interpreting.

defined class Person
defined object Person
Copy the code

First of all should be from scala. Reflect. The runtime. Gets a runtime JavaMirror universe. Here are some details need to pay attention to, in this case do not directly introduce import scala, reflect the runtime. Universe. _ (because some implicit conversion can cause some conflict). The author receives universe using the variable ru (short for Runtime).

scala> val ru = scala.reflect.runtime.universe
ru: scala.reflect.api.JavaUniverse = scala.reflect.runtime.JavaUniverse@3c9c0d96

scala> val mirror = ru.runtimeMirror(getClass.getClassLoader)
mirror: ru.Mirror = JavaMirror with scala.tools.nsc.interpreter.IMain$TranslatingClassLoader@6fb65c1f ...
Copy the code

1.4.1 Reflection Examples

It is possible to reflect an instance by calling mirror.reflect(

), such as trying to reflect a Person instance:

scala> val im = mirror.reflect(new Person("Wangfang".20))
im: reflect.runtime.universe.InstanceMirror = instance mirror for Person@39b626e5
Copy the code

It will return an InstanceMirror. For the mirror of this instance, you can also try to get two child mirrors, one for the internal methods and the other for the internal properties of the instance:

  1. MethodMirror: mirror image used to reflect methods inside the instance.
  2. FieldMirror: mirror image used to reflect the internal properties of the instance.

The idea behind these two mirrors is to get the identifier first and then go to the corresponding mirror to get the value.

scala> val term: ru.TermSymbol = ru.typeOf[Person].decl(ru.TermName("name")).asTerm
term: ru.TermSymbol = method name

scala> val nameValue: ru.FieldMirror = im.reflectField(term)
nameValue: ru.FieldMirror = field mirror for private[this] var name: String (bound to Person@48f4713c)

scala> println(s"this instance's name value = ${nameValue.get}")
this instance's name value = Wangfang
Copy the code

The above interactive commands are written in a. Scala file as follows:

/ / don't introduced directly into scala. Reflect. Runtime. Universe. _, otherwise it will introduce some implicit value of the accident
//ru : runtime universe
val ru : JavaUniverse = scala.reflect.runtime.universe

// Get the mirror entry for runtime reflection
val mirror : ru.Mirror = ru.runtimeMirror(getClass.getClassLoader)

// Convert the input string "name" to an identifier.
val term: ru.TermSymbol = ru.typeOf[Person].decl(ru.TermName("name")).asTerm

// Get the mirror of the property by calling reflectField(term). If the property has a value, it can be obtained by get or set.
val nameValue: ru.FieldMirror = mirror.reflect(new Person("Wangfang".20)).reflectField(term)
println(s"this instance's name value = ${nameValue.get}")
Copy the code

Similarly, if you want to reflect an instance’s methods, you first get its MethodMIrror based on its identifier and call it through the Apply method. It allows you to receive mutable parameters, depending on what parameters are required by the reflected method.

/ / don't introduced directly into scala. Reflect. Runtime. Universe. _, otherwise it will introduce some implicit value of the accident
//ru : runtime universe
val ru = scala.reflect.runtime.universe

// Get the mirror entry for runtime reflection
val mirror  = ru.runtimeMirror(getClass.getClassLoader)

// Convert the input string "name" to an identifier.
val term = ru.typeOf[Person].decl(ru.TermName("unsafe")).asMethod

// Passed in by calling reflectField(term)
val im: ru.InstanceMirror = mirror.reflect(new Person("Wangfang".20))

// By calling this method through reflection, Apply can receive the input arguments that the method needs.
im.reflectMethod(term).apply()
Copy the code

We also note that even though unsafe methods are private, they can still be retrieved and run in reflected form (the reflected method is actually executed by applying ()).

1.4.2 Reflection Constructor

If you want to reflect the Person class and get to the constructor, you need to by ru (namely the scala. Reflect. Runtime. Universe) access to the type of ClassSymbol MethodSymbol and constructor method.

val clazzPerson: ru.ClassSymbol = ru.typeOf[Person].typeSymbol.asClass

val cm: ru.ClassMirror = mirror.reflectClass(clazzPerson)

// If there are multiple constructors, the situation can be complicated, need to use ↓
// ru.typeof [Person].decl(ru.termnames.constructor).asterm.alternative (x).asMethod selects the x CONSTRUCTOR as an alternative in the list.
val ctorPerson: ru.MethodSymbol = ru.typeOf[Person].decl(ru.termNames.CONSTRUCTOR).asMethod

cm.reflectConstructor(ctorPerson)
Copy the code

1.4.3 Reflection associated (singleton) objects

ModuleMirror is used to reflect a singleton object of a class, where ru.typeof [_] type arguments need to be person.type instead of Person.

val objectPerson: ru.ModuleSymbol = ru.typeOf[Person.type].termSymbol.asModule

val mm: ru.ModuleMirror = mirror.reflectModule(objectPerson)

mm.instance.asInstanceOf[Person.type].greet()
Copy the code

1.5 Resources: Runtime reflection

  • Scala Reflection -1
  • Scala Reflection -2
  • [CSDN] What is TypeTag and when should you use it?
  • The path to Scala functional programming
  • Scala Reflection Environment, Universe, and Mirror
  • A brief introduction to Scala runtime reflection
  • Scala Reflect-mirrors,ClassTag,TypeTag and WeakTypeTag
  • CSDN Manifests in Scala TypeTags and Sse
  • Preliminary learning of Type and Class in Scala
  • Scala Type: Type vs.Class