Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The relationship between Type Symbol and Mirror of Scala reflection

The Scala reflection is really complicated. It contains type symbol and mirror. Could you tell me the relationship between them?

like image 562
lxy Avatar asked Dec 07 '22 16:12

lxy


1 Answers

When working with the Scala Reflection API you encounter a lot more types that what you're used to if you've used the Java Reflection API. Given an example scenario where you start with a String containing a fully qualified class name of a class, these are the types you are likely to encounter:

  • Universe: Scala supports both runtime and compile time reflection. You choose what kind of reflection you're doing by importing from the corresponding universe. For runtime reflection, this corresponds to the scala.reflect.runtime package, for compile time reflection it corresponds to the scala.reflect.macros package. This answer focus on the former.

    Like Java you typically start any reflection by choosing which ClassLoader's classes you want to reflect on. Scala provides a shortcut for using the ClassLoader of the current class: scala.reflect.runtime.currentMirror, this gives you a Mirror (more on mirrors later). Many JVM applications use just a single class loader, so this is a common entry point for the Scala Reflection API. Since you're importing from runtime you're now in that Universe.

  • Symbols: Symbols contain static metadata about whatever you want to reflect. This includes anything you can think of: is this thing a case class, is it a field, is it a class, what are the type parameters, is it abstract, etc. You may not query anything that might depend on the current lexical scope, for example what members a class has. You may also not interact with the thing you reflect on in any way (like accessing fields or calling methods). You can just query metadata.

    Lexical scope is everything you can "see" at the place you're doing reflection, excluding implicit scope (see this SO for a treatment of different scopes). How can members of a class vary with lexical scope? Imagine an abstract class with a single def foo: String. The name foo might be bound to a def in one context (giving you a MethodSymbol should you query for it) or it could be bound to a val in another context (giving you a TermSymbol). When working with Symbols it's common to explicitly have to state what kind of symbol you expect, you do this through the methods .asTerm, .asMethod, .asClass etc.

    Continuing the String example we started with. You use the Mirror to derive a ClassSymbol describing the class: currentMirror.staticClass(myString).

  • Types: Types lets you query information about the type the symbol refers to in the current lexical context. You typically use Types for two things: querying what vars, vals and defs there are, and querying type relationships (e.g. is this type a subclass of that type). There are two ways of getting hold of a Type. Either through a TypeSymbol (ClassSymbol is a TypeSymbol) or through a TypeTag.

    Continuing the example you would invoke the .toType method on the symbol you got to get the Type.

  • Scopes: When you ask a Type for .members or .decl—this is what gives you terms (vars and vals) and methods—you get a list of Symbols of the members in the current lexical scope. This list is kept in a type MemberScope, it's just a glorified List[Symbol].

    In our example with the abstract class above, this list would contain a TermSymbol or a MethodSymbol for the name foo depending on the current scope.

  • Names: Names come in two flavors: TermName and TypeName. It's just a wrapper of a String. You can use the type to determine what is named by any Name.
  • Mirrors: Finally mirrors are what you use to interact with "something". You typically start out with a Symbol and then use that symbol to derive symbols for the methods, constructors or fields you want to interact with. When you have the symbols you need, you use currentMirror to create mirrors for those symbols. Mirrors lets you call constructors (ClassMirror), access fields (FieldMirror) or invoke methods (MethodMirror). You may not use mirrors to query metadata about the thing being reflected.

So putting an example together reflecting the description above, this is how you would search for fields, invoke constructors and read a val, given a String with the fully qualified class name:

// Do runtime reflection on classes loaded by current ClassLoader
val currentMirror: universe.Mirror = scala.reflect.runtime.currentMirror

// Use symbols to navigate to pick out the methods and fields we want to invoke
// Notice explicit symbol casting with the `.as*` methods.
val classSymbol: universe.ClassSymbol = currentMirror.staticClass("com.example.Foo")
val constructorSymbol: universe.MethodSymbol = classSymbol.primaryConstructor.asMethod
val fooSymbol: Option[universe.TermSymbol] = classSymbol.toType.members.find(_.name.toString == "foo").map(_.asTerm)

// Get mirrors for performing constructor and field invocations
val classMirror: universe.ClassMirror = currentMirror.reflectClass(classSymbol)
val fooInstance: Foo = classMirror.reflectConstructor(constructorSymbol).apply().asInstanceOf[Foo]
val instanceMirror: universe.InstanceMirror = currentMirror.reflect(fooInstance)

// Do the actual invocation
val fooValue: String = instanceMirror.reflectField(fooSymbol.get).get.asInstanceOf[String]
println(fooValue) // Prints the value of the val "foo" of the object "fooInstance"
like image 74
gogstad Avatar answered Apr 24 '23 02:04

gogstad