[转]Scala: Reflection(EXPERIMENTAL)
程序员文章站
2022-05-13 08:13:49
...
86d606bfb2664dc0ab2819db400ab4a2
原文:http://docs.scala-lang.org/overviews/reflection/overview.html
Overview
EXPERIMENTAL
Heather Miller, Eugene Burmako, Philipp Haller
Reflection is the ability of a program to inspect, and possibly even modify itself. It has a long history across object-oriented, functional, and logic programming paradigms. While some languages are built around reflection as a guiding principle, many languages progressively evolve their reflection abilities over time.
Reflection involves the ability to reify (ie. make explicit) otherwise-implicit elements of a program. These elements can be either static program elements like classes, methods, or expressions, or dynamic elements like the current continuation or execution events such as method invocations and field accesses. One usually distinguishes between compile-time and runtime reflection depending on when the reflection process is performed. Compile-time reflection is a powerful way to develop program transformers and generators, while runtime reflection is typically used to adapt the language semantics or to support very late binding between software components.
Until 2.10, Scala has not had any reflection capabilities of its own. Instead, one could use part of the Java reflection API, namely that dealing with providing the ability to dynamically inspect classes and objects and access their members. However, many Scala-specific elements are unrecoverable under standalone Java reflection, which only exposes Java elements (no functions, no traits) and types (no existential, higher-kinded, path-dependent and abstract types). In addition, Java reflection is also unable to recover runtime type info of Java types that are generic at compile-time; a restriction that carried through to runtime reflection on generic types in Scala.
In Scala 2.10, a new reflection library was introduced not only to address the shortcomings of Java’s runtime reflection on Scala-specific and generic types, but to also add a more powerful toolkit of general reflective capabilities to Scala. Along with full-featured runtime reflection for Scala types and generics, Scala 2.10 also ships with compile-time reflection capabilities, in the form of macros, as well as the ability to reify Scala expressions into abstract syntax trees.
Runtime Reflection
What is runtime reflection? Given a type or instance of some object at runtime, reflection is the ability to:
inspect the type of that object, including generic types,
to instantiate new objects,
or to access or invoke members of that object.
Let’s jump in and see how to do each of the above with a few examples.
Examples
Inspecting a Runtime Type (Including Generic Types at Runtime)
As with other JVM languages, Scala’s types are erased at compile time. This means that if you were to inspect the runtime type of some instance, that you might not have access to all type information that the Scala compiler has available at compile time.
TypeTags can be thought of as objects which carry along all type information available at compile time, to runtime. Though, it’s important to note that TypeTags are always generated by the compiler. This generation is triggered whenever an implicit parameter or context bound requiring aTypeTag is used. This means that, typically, one can only obtain a TypeTag using implicit parameters or context bounds.
For example, using context bounds:
scala> import scala.reflect.runtime.{universe => ru}
import scala.reflect.runtime.{universe=>ru}
scala> val l = List(1,2,3)
l: List[Int] = List(1, 2, 3)
scala> def getTypeTag[T: ru.TypeTag](obj: T) = ru.typeTag[T]
getTypeTag: [T](obj: T)(implicit evidence$1: ru.TypeTag[T])ru.TypeTag[T]
scala> val theType = getTypeTag(l).tpe
theType: ru.Type = List[Int]
In the above, we first import scala.reflect.runtime.universe (it must always be imported in order to use TypeTags), and we create a List[Int] called l. Then, we define a methodgetTypeTag which has a type parameter T that has a context bound (as the REPL shows, this is equivalent to defining an implicit “evidence” parameter, which causes the compiler to generate aTypeTag for T). Finally, we invoke our method with l as its parameter, and call tpe which returns the type contained in the TypeTag. As we can see, we get the correct, complete type (includingList’s concrete type argument), List[Int].
Once we have obtained the desired Type instance, we can inspect it, e.g.:
scala> val decls = theType.declarations.take(10)
decls: Iterable[ru.Symbol] = List(constructor List, method companion, method isEmpty, method head, method tail, method ::, method :::, method reverse_:::, method mapConserve, method ++)
Instantiating a Type at Runtime
Types obtained through reflection can be instantiated by invoking their constructor using an appropriate “invoker” mirror (mirrors are expanded upon below). Let’s walk through an example using the REPL:
scala> case class Person(name: String)
defined class Person
scala> val m = ru.runtimeMirror(getClass.getClassLoader)
m: reflect.runtime.universe.Mirror = JavaMirror with ...
In the first step we obtain a mirror m which makes all classes and types available that are loaded by the current classloader, including class Person.
scala> val classPerson = ru.typeOf[Person].typeSymbol.asClass
classPerson: reflect.runtime.universe.ClassSymbol = class Person
scala> val cm = m.reflectClass(classPerson)
cm: reflect.runtime.universe.ClassMirror = class mirror for Person (bound to null)
The second step involves obtaining a ClassMirror for class Person using the reflectClassmethod. The ClassMirror provides access to the constructor of class Person.
scala> val ctor = ru.typeOf[Person].declaration(ru.nme.CONSTRUCTOR).asMethod
ctor: reflect.runtime.universe.MethodSymbol = constructor Person
The symbol for Persons constructor can be obtained using only the runtime universe ru by looking it up in the declarations of type Person.
scala> val ctorm = cm.reflectConstructor(ctor)
ctorm: reflect.runtime.universe.MethodMirror = constructor mirror for Person.<init>(name: String): Person (bound to null)
scala> val p = ctorm("Mike")
p: Any = Person(Mike)
Accessing and Invoking Members of Runtime Types
In general, members of runtime types are accessed using an appropriate “invoker” mirror (mirrors are expanded upon below). Let’s walk through an example using the REPL:
scala> case class Purchase(name: String, orderNumber: Int, var shipped: Boolean)
defined class Purchase
scala> val p = Purchase("Jeff Lebowski", 23819, false)
p: Purchase = Purchase(Jeff Lebowski,23819,false)
In this example, we will attempt to get and set the shipped field of Purchase p, reflectively.
scala> import scala.reflect.runtime.{universe => ru}
import scala.reflect.runtime.{universe=>ru}
scala> val m = ru.runtimeMirror(p.getClass.getClassLoader)
m: reflect.runtime.universe.Mirror = JavaMirror with ...
As we did in the previous example, we’ll begin by obtaining a mirror m, which makes all classes and types available that are loaded by the classloader that also loaded the class of p (Purchase), which we need in order to access member shipped.
scala> val shippingTermSymb = ru.typeOf[Purchase].declaration(ru.newTermName("shipped")).asTerm
shippingTermSymb: reflect.runtime.universe.TermSymbol = method shipped
We now look up the declaration of the shipped field, which gives us a TermSymbol (a type ofSymbol). We’ll need to use this Symbol later to obtain a mirror that gives us access to the value of this field (for some instance).
scala> val im = m.reflect(p)
im: reflect.runtime.universe.InstanceMirror = instance mirror for Purchase(Jeff Lebowski,23819,false)
scala> val shippingFieldMirror = im.reflectField(shippingTermSymb)
shippingFieldMirror: reflect.runtime.universe.FieldMirror = field mirror for Purchase.shipped (bound to Purchase(Jeff Lebowski,23819,false))
In order to access a specific instance’s shipped member, we need a mirror for our specific instance, p’s instance mirror, im. Given our instance mirror, we can obtain a FieldMirror for anyTermSymbol representing a field of p’s type.
Now that we have a FieldMirror for our specific field, we can use methods get and set to get/set our specific instance’s shipped member. Let’s change the status of shipped to true.
scala> shippingFieldMirror.get
res7: Any = false
scala> shippingFieldMirror.set(true)
scala> shippingFieldMirror.get
res9: Any = true
Runtime Classes in Java vs. Runtime Types in Scala
Those who are comfortable using Java reflection to obtain Java Class instances at runtime might have noticed that, in Scala, we instead obtain runtime types.
The REPL-run below shows a very simple scenario where using Java reflection on Scala classes might return surprising or incorrect results.
First, we define a base class E with an abstract type member T, and from it, we derive two subclasses, C and D.
scala> class E {
| type T
| val x: Option[T] = None
| }
defined class E
scala> class C extends E
defined class C
scala> class D extends C
defined class D
Then, we create an instance of both C and D, meanwhile making type member T concrete (in both cases, String)
scala> val c = new C { type T = String }
c: C{type T = String} = $anon$1@7113bc51
scala> val d = new D { type T = String }
d: D{type T = String} = $anon$1@46364879
Now, we use methods getClass and isAssignableFrom from Java Reflection to obtain an instance of java.lang.Class representing the runtime classes of c and d, and then we test to see that d’s runtime class is a subclass of c’s runtime representation.
scala> c.getClass.isAssignableFrom(d.getClass)
res6: Boolean = false
Since above, we saw that D extends C, this result is a bit surprising. In performing this simple runtime type check, one would expect the result of the question “is the class of d a subclass of the class of c?” to be true. However, as you might’ve noticed above, when c and d are instantiated, the Scala compiler actually creates anonymous subclasses of C and D, respectively. This is due to the fact that the Scala compiler must translate Scala-specific (i.e., non-Java) language features into some equivalent in Java bytecode in order to be able to run on the JVM. Thus, the Scala compiler often creates synthetic classes (i.e. automatically-generated classes) that are used at runtime in place of user-defined classes. This is quite commonplace in Scala and can be observed when using Java reflection with a number of Scala features, e.g. closures, type members, type refinements, local classes, etc.
In situations like these, we can instead use Scala reflection to obtain precise runtime types of these Scala objects. Scala runtime types carry along all type info from compile-time, avoiding these types mismatches between compile-time and run-time.
Below, we use define a method which uses Scala reflection to get the runtime types of its arguments, and then checks the subtyping relationship between the two. If its first argument’s type is a subtype of its second argument’s type, it returns true.
scala> import scala.reflect.runtime.{universe => ru}
import scala.reflect.runtime.{universe=>ru}
scala> def m[T: ru.TypeTag, S: ru.TypeTag](x: T, y: S): Boolean = {
| val leftTag = ru.typeTag[T]
| val rightTag = ru.typeTag[S]
| leftTag.tpe <:< rightTag.tpe
| }
m: [T, S](x: T, y: S)(implicit evidence$1: reflect.runtime.universe.TypeTag[T], implicit evidence$2: reflect.runtime.universe.TypeTag[S])Boolean
scala> m(d, c)
res9: Boolean = true
As we can see, we now get the expected result– d’s runtime type is indeed a subtype of c’s runtime type.
Compile-time Reflection
Scala reflection enables a form of metaprogramming which makes it possible for programs to modifythemselves at compile time. This compile-time reflection is realized in the form of macros, which provide the ability to execute methods that manipulate abstract syntax trees at compile-time.
A particularly interesting aspect of macros is that they are based on the same API used also for Scala’s runtime reflection, provided in package scala.reflect.api. This enables the sharing of generic code between macros and implementations that utilize runtime reflection.
Note that the macros guide focuses on macro specifics, whereas this guide focuses on the general aspects of the reflection API. Many concepts directly apply to macros, though, such as abstract syntax trees which are discussed in greater detail in the section on Symbols, Trees, and Types.
Environment
All reflection tasks require a proper environment to be set up. This environment differs based on whether the reflective task is to be done at run time or at compile time. The distinction between an environment to be used at run time or compile time is encapsulated in a so-called universe. Another important aspect of the reflective environment is the set of entities that we have reflective access to. This set of entities is determined by a so-called mirror.
Mirrors not only determine the set of entities that can be accessed reflectively. They also provide reflective operations to be performed on those entities. For example, in runtime reflection an invoker mirror can be used to invoke a method or constructor of a class.
Universes
Universe is the entry point to Scala reflection. A universe provides an interface to all the principal concepts used in reflection, such as Types, Trees, and Annotations. For more details, see the section of this guide on Universes, or the Universes API docs in package scala.reflect.api.
To use most aspects of Scala reflection, including most code examples provided in this guide, you need to make sure you import a Universe or the members of a Universe. Typically, to use runtime reflection, one can import all members of scala.reflect.runtime.universe, using a wildcard import:
import scala.reflect.runtime.universe._
Mirrors
Mirrors are a central part of Scala Reflection. All information provided by reflection is made accessible through these so-called mirrors. Depending on the type of information to be obtained, or the reflective action to be taken, different flavors of mirrors must be used.
For more details, see the section of this guide on Mirrors, or the Mirrors API docs in packagescala.reflect.api.
Environment, Universes, and Mirrors
EXPERIMENTAL
Environment
The reflection environment differs based on whether the reflective task is to be done at run time or at compile time. The distinction between an environment to be used at run time or compile time is encapsulated in a so-called universe. Another important aspect of the reflective environment is the set of entities that we have reflective access to. This set of entities is determined by a so-calledmirror.
For example, the entities accessible through runtime reflection are made available by aClassloaderMirror. This mirror provides only access to entities (packages, types, and members) loaded by a specific classloader.
Mirrors not only determine the set of entities that can be accessed reflectively. They also provide reflective operations to be performed on those entities. For example, in runtime reflection an invoker mirror can be used to invoke a method or constructor of a class.
Universes
There are two principal types of universes– since there exists both runtime and compile-time reflection capabilities, one must use the universe that corresponds to whatever the task is at hand. Either:
scala.reflect.runtime.universe for runtime reflection, or
scala.reflect.macros.Universe for compile-time reflection.
A universe provides an interface to all the principal concepts used in reflection, such as Types,Trees, and Annotations.
Mirrors
All information provided by reflection is made accessible through mirrors. Depending on the type of information to be obtained, or the reflective action to be taken, different flavors of mirrors must be used. Classloader mirrors can be used to obtain representations of types and members. From a classloader mirror, it’s possible to obtain more specialized invoker mirrors (the most commonly-used mirrors), which implement reflective invocations, such as method or constructor calls and field accesses.
Summary:
“Classloader” mirrors. These mirrors translate names to symbols (via methodsstaticClass/staticModule/staticPackage).
“Invoker” mirrors. These mirrors implement reflective invocations (via methodsMethodMirror.apply, FieldMirror.get, etc.). These “invoker” mirrors are the types of mirrors that are most commonly used.
Runtime Mirrors
The entry point to mirrors for use at runtime is via ru.runtimeMirror(<classloader>), where ruis scala.reflect.runtime.universe.
The result of a scala.reflect.api.JavaMirrors#runtimeMirror call is a classloader mirror, of type scala.reflect.api.Mirrors#ReflectiveMirror, which can load symbols by name.
A classloader mirror can create invoker mirrors (includingscala.reflect.api.Mirrors#InstanceMirror, scala.reflect.api.Mirrors#MethodMirror,scala.reflect.api.Mirrors#FieldMirror, scala.reflect.api.Mirrors#ClassMirror, andscala.reflect.api.Mirrors#ModuleMirror).
Examples of how these two types of mirrors interact are available below.
Types of Mirrors, Their Use Cases & Examples
A ReflectiveMirror is used for loading symbols by name, and as an entry point into invoker mirrors. Entry point: val m = ru.runtimeMirror(<classloader>). Example:
scala> val ru = scala.reflect.runtime.universe
ru: scala.reflect.api.JavaUniverse = ...
scala> val m = ru.runtimeMirror(getClass.getClassLoader)
m: reflect.runtime.universe.Mirror = JavaMirror ...
An InstanceMirror is used for creating invoker mirrors for methods and fields and for inner classes and inner objects (modules). Entry point: val im = m.reflect(<value>). Example:
scala> class C { def x = 2 }
defined class C
scala> val im = m.reflect(new C)
im: reflect.runtime.universe.InstanceMirror = instance mirror for C@3442299e
A MethodMirror is used for invoking instance methods (Scala only has instance methods– methods of objects are instance methods of object instances, obtainable viaModuleMirror.instance). Entry point: val mm = im.reflectMethod(<method symbol>). Example:
scala> val methodX = ru.typeOf[C].declaration(ru.newTermName("x")).asMethod
methodX: reflect.runtime.universe.MethodSymbol = method x
scala> val mm = im.reflectMethod(methodX)
mm: reflect.runtime.universe.MethodMirror = method mirror for C.x: scala.Int (bound to C@3442299e)
scala> mm()
res0: Any = 2
A FieldMirror is used for getting/setting instance fields (like methods, Scala only has instance fields, see above). Entry point: val fm = im.reflectMethod(<field or accessor symbol>). Example:
scala> class C { val x = 2; var y = 3 }
defined class C
scala> val m = ru.runtimeMirror(getClass.getClassLoader)
m: reflect.runtime.universe.Mirror = JavaMirror ...
scala> val im = m.reflect(new C)
im: reflect.runtime.universe.InstanceMirror = instance mirror for C@5f0c8ac1
scala> val fieldX = ru.typeOf[C].declaration(ru.newTermName("x")).asTerm.accessed.asTerm
fieldX: reflect.runtime.universe.TermSymbol = value x
scala> val fmX = im.reflectField(fieldX)
fmX: reflect.runtime.universe.FieldMirror = field mirror for C.x (bound to C@5f0c8ac1)
scala> fmX.get
res0: Any = 2
scala> fmX.set(3)
scala> val fieldY = ru.typeOf[C].declaration(ru.newTermName("y")).asTerm.accessed.asTerm
fieldY: reflect.runtime.universe.TermSymbol = variable y
scala> val fmY = im.reflectField(fieldY)
fmY: reflect.runtime.universe.FieldMirror = field mirror for C.y (bound to C@5f0c8ac1)
scala> fmY.get
res1: Any = 3
scala> fmY.set(4)
scala> fmY.get
res2: Any = 4
A ClassMirror is used for creating invoker mirrors for constructors. Entry points: for static classesval cm1 = m.reflectClass(<class symbol>), for inner classes val mm2 = im.reflectClass(<class symbol>). Example:
scala> case class C(x: Int)
defined class C
scala> val m = ru.runtimeMirror(getClass.getClassLoader)
m: reflect.runtime.universe.Mirror = JavaMirror ...
scala> val classC = ru.typeOf[C].typeSymbol.asClass
classC: reflect.runtime.universe.Symbol = class C
scala> val cm = m.reflectClass(classC)
cm: reflect.runtime.universe.ClassMirror = class mirror for C (bound to null)
scala> val ctorC = ru.typeOf[C].declaration(ru.nme.CONSTRUCTOR).asMethod
ctorC: reflect.runtime.universe.MethodSymbol = constructor C
scala> val ctorm = cm.reflectConstructor(ctorC)
ctorm: reflect.runtime.universe.MethodMirror = constructor mirror for C.<init>(x: scala.Int): C (bound to null)
scala> ctorm(2)
res0: Any = C(2)
A ModuleMirror is used for accessing instances of singleton objects. Entry points: for static objects val mm1 = m.reflectModule(<module symbol>), for inner objects val mm2 = im.reflectModule(<module symbol>). Example:
scala> object C { def x = 2 }
defined module C
scala> val m = ru.runtimeMirror(getClass.getClassLoader)
m: reflect.runtime.universe.Mirror = JavaMirror ...
scala> val objectC = ru.typeOf[C.type].termSymbol.asModule
objectC: reflect.runtime.universe.ModuleSymbol = object C
scala> val mm = m.reflectModule(objectC)
mm: reflect.runtime.universe.ModuleMirror = module mirror for C (bound to null)
scala> val obj = mm.instance
obj: Any = C$@1005ec04
Compile-Time Mirrors
Compile-time mirrors make use of only classloader mirrors to load symbols by name.
The entry point to classloader mirrors is via scala.reflect.macros.Context#mirror. Typical methods which use classloader mirrors include scala.reflect.api.Mirror#staticClass,scala.reflect.api.Mirror#staticModule, and scala.reflect.api.Mirror#staticPackage. For example:
import scala.reflect.macros.Context
case class Location(filename: String, line: Int, column: Int)
object Macros {
def currentLocation: Location = macro impl
def impl(c: Context): c.Expr[Location] = {
import c.universe._
val pos = c.macroApplication.pos
val clsLocation = c.mirror.staticModule("Location") // get symbol of "Location" object
c.Expr(Apply(Ident(clsLocation), List(Literal(Constant(pos.source.path)), Literal(Constant(pos.line)), Literal(Constant(pos.column)))))
}
}
Of note: There are several high-level alternatives that one can use to avoid having to manually lookup symbols. For example, typeOf[Location.type].termSymbol (ortypeOf[Location].typeSymbol if we needed a ClassSymbol), which are typesafe since we don’t have to use strings to lookup the symbol.
Symbols, Trees, and Types
EXPERIMENTAL
Symbols
Symbols are used to establish bindings between a name and the entity it refers to, such as a class or a method. Anything you define and can give a name to in Scala has an associated symbol.
Symbols contain all available information about the declaration of an entity (class/object/traitetc.) or a member (vals/vars/defs etc.), and as such are an integral abstraction central to both runtime reflection and compile-time reflection (macros).
A symbol can provide a wealth of information ranging from the basic name method available on all symbols to other, more involved, concepts such as getting the baseClasses from ClassSymbol. Other common use cases of symbols include inspecting members’ signatures, getting type parameters of a class, getting the parameter type of a method or finding out the type of a field.
The Symbol Owner Hierarchy
Symbols are organized in a hierarchy. For example, a symbol that represents a parameter of a method is owned by the corresponding method symbol, a method symbol is owned by its enclosing class, trait, or object, a class is owned by a containing package and so on.
If a symbol does not have an owner, for example, because it refers to a top-level entity, such as a top-level package, then its owner is the special NoSymbol singleton object. Representing a missing symbol, NoSymbol is commonly used in the API to denote an empty or default value. Accessing theowner of NoSymbol throws an exception. See the API docs for the general interface provided by type Symbol
TypeSymbols
A TypeSymbol represents type, class, and trait declarations, as well as type parameters. Interesting members that do not apply to the more specific ClassSymbols, includeisAbstractType, isContravariant, and isCovariant.
ClassSymbol: Provides access to all information contained in a class or trait declaration, e.g.,name, modifiers (isFinal, isPrivate, isProtected, isAbstractClass, etc.),baseClasses, and typeParams.
TermSymbols
The type of term symbols representing val, var, def, and object declarations as well as packages and value parameters.
MethodSymbol: The type of method symbols representing def declarations (subclass ofTermSymbol). It supports queries like checking whether a method is a (primary) constructor, or whether a method supports variable-length argument lists.
ModuleSymbol: The type of module symbols representing object declarations. It allows looking up the class implicitly associated with the object definition via member moduleClass. The opposite look up is also possible. One can go back from a module class to the associated module symbol by inspecting its selfType.termSymbol.
Symbol Conversions
There can be situations where one uses a method that returns an instance of the general Symboltype. In cases like these, it’s possible to convert the more general Symbol type obtained to the specific, more specialized symbol type needed.
Symbol conversions, such as asClass or asMethod, are used to convert to a more specific subtype of Symbol as appropriate (if you want to use the MethodSymbol interface, for example).
For example,
scala> import reflect.runtime.universe._
import reflect.runtime.universe._
scala> class C[T] { def test[U](x: T)(y: U): Int = ??? }
defined class C
scala> val testMember = typeOf[C[Int]].member(newTermName("test"))
testMember: reflect.runtime.universe.Symbol = method test
In this case, member returns an instance of Symbol, not MethodSymbol as one might expect. Thus, we must use asMethod to ensure that we obtain a MethodSymbol
scala> testMember.asMethod
res0: reflect.runtime.universe.MethodSymbol = method test
Free symbols
The two symbol types FreeTermSymbol and FreeTypeSymbol have a special status, in the sense that they refer to symbols whose available information is not complete. These symbols are generated in some cases during reification (see the corresponding section about reifying trees for more background). Whenever reification cannot locate a symbol (meaning that the symbol is not available in the corresponding class file, for example, because the symbol refers to a local class), it reifies it as a so-called “free type”, a synthetic dummy symbol that remembers the original name and owner and has a surrogate type signature that closely follows the original. You can check whether a symbol is a free type by calling sym.isFreeType. You can also get a list of all free types referenced by a tree and its children by calling tree.freeTypes. Finally, you can get warnings when reification produces free types by using -Xlog-free-types.
Types
As its name suggests, instances of Type represent information about the type of a corresponding symbol. This includes its members (methods, fields, type aliases, abstract types, nested classes, traits, etc.) either declared directly or inherited, its base types, its erasure and so on. Types also provide operations to test for type conformance or equivalence.
Instantiating Types
In general, there are three ways to instantiate a Type.
via method typeOf on scala.reflect.api.TypeTags, which is mixed into Universe(simplest and most common).
Standard Types, such as Int, Boolean, Any, or Unit are accessible through the available universe.
Manual instantiation using factory methods such as typeRef or polyType onscala.reflect.api.Types, (not recommended).
Instantiating Types With typeOf
To instantiate a type, most of the time, the scala.reflect.api.TypeTags#typeOf method can be used. It takes a type argument and produces a Type instance which represents that argument. For example:
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> typeOf[List[Int]]
res0: reflect.runtime.universe.Type = scala.List[Int]
In this example, a scala.reflect.api.Types$TypeRef is returned, which corresponds to the type constructor List, applied to the type argument Int.
Note, however, that this approach requires one to specify by hand the type we’re trying to instantiate. What if we’re interested in obtaining an instance of Type that corresponds to some arbitrary instance? One can simply define a method with a context bound on the type parameter– this generates a specialized TypeTag for us, which we can use to obtain the type of our arbitrary instance:
scala> def getType[T: TypeTag](obj: T) = typeOf[T]
getType: [T](obj: T)(implicit evidence$1: reflect.runtime.universe.TypeTag[T])reflect.runtime.universe.Type
scala> getType(List(1,2,3))
res1: reflect.runtime.universe.Type = List[Int]
scala> class Animal; class Cat extends Animal
defined class Animal
defined class Cat
scala> val a = new Animal
a: Animal = Animal@21c17f5a
scala> getType(a)
res2: reflect.runtime.universe.Type = Animal
scala> val c = new Cat
c: Cat = Cat@2302d72d
scala> getType(c)
res3: reflect.runtime.universe.Type = Cat
Note: Method typeOf does not work for types with type parameters, such as typeOf[List[A]]where A is a type parameter. In this case, one can usescala.reflect.api.TypeTags#weakTypeOf instead. For more details, see the TypeTags section of this guide.
Standard Types
Standard types, such as Int, Boolean, Any, or Unit, are accessible through a universe’sdefinitions member. For example:
scala> import scala.reflect.runtime.universe
import scala.reflect.runtime.universe
scala> val intTpe = universe.definitions.IntTpe
intTpe: reflect.runtime.universe.Type = Int
The list of standard types is specified in trait StandardTypes inscala.reflect.api.StandardDefinitions.
Common Operations on Types
Types are typically used for type conformance tests or are queried for members. The three main classes of operations performed on types are:
Checking the subtyping relationship between two types.
Checking for equality between two types.
Querying a given type for certain members or inner types.
Subtyping Relationships
Given two Type instances, one can easily test whether one is a subtype of the other using <:<(and in exceptional cases, weak_<:<, explained below)
scala> import scala.reflect.runtime.universe._
import scala-lang.reflect.runtime.universe._
scala> class A; class B extends A
defined class A
defined class B
scala> typeOf[A] <:< typeOf[B]
res0: Boolean = false
scala> typeOf[B] <:< typeOf[A]
res1: Boolean = true
Note that method weak_<:< exists to check for weak conformance between two types. This is typically important when dealing with numeric types.
Scala’s numeric types abide by the following ordering (section 3.5.3 of the Scala language specification):
In some situations Scala uses a more general conformance relation. A type S weakly conforms to a type T, written S <:w T, if S<:T or both S and T are primitive number types and S precedes T in the following ordering:
| Weak Conformance Relations | ————————— | Byte <:w Short | | Short <:w Int | |Char <:w Int | | Int <:w Long | | Long <:w Float | | Float <:w Double |
For example, weak conformance is used to determine the type of the following if-expression:
scala> if (true) 1 else 1d
res2: Double = 1.0
In the if-expression shown above, the result type is defined to be the weak least upper bound of the two types (i.e., the least upper bound with respect to weak conformance).
Thus, since Double is defined to be the least upper bound with respect to weak conformance between Int and Double (according to the spec, shown above), Double is inferred as the type of our example if-expression.
Note that method weak_<:< checks for weak conformance (as opposed to <:< which checks for conformance without taking into consideration weak conformance relations in section 3.5.3 of the spec) and thus returns the correct result when inspecting conformance relations between numeric types Int and Double:
scala> typeOf[Int] weak_<:< typeOf[Double]
res3: Boolean = true
scala> typeOf[Double] weak_<:< typeOf[Int]
res4: Boolean = false
Whereas using <:< would incorrectly report that Int and Double do not conform to each other in any way:
scala> typeOf[Int] <:< typeOf[Double]
res5: Boolean = false
scala> typeOf[Double] <:< typeOf[Int]
res6: Boolean = false
Type Equality
Similar to type conformance, one can easily check the equality of two types. That is, given two arbitrary types, one can use method =:= to see if both denote the exact same compile-time type.
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> def getType[T: TypeTag](obj: T) = typeOf[T]
getType: [T](obj: T)(implicit evidence$1: reflect.runtime.universe.TypeTag[T])reflect.runtime.universe.Type
scala> class A
defined class A
scala> val a1 = new A; val a2 = new A
a1: A = A@cddb2e7
a2: A = A@2f0c624a
scala> getType(a1) =:= getType(a2)
res0: Boolean = true
Note that the precise type info must be the same for both instances. In the following code snippet, for example, we have two instances of List with different type arguments.
scala> getType(List(1,2,3)) =:= getType(List(1.0, 2.0, 3.0))
res1: Boolean = false
scala> getType(List(1,2,3)) =:= getType(List(9,8,7))
res2: Boolean = true
Also important to note is that =:= should always be used to compare types for equality. That is, never use ==, as it can’t check for type equality in the presence of type aliases, whereas =:= can:
scala> type Histogram = List[Int]
defined type alias Histogram
scala> typeOf[Histogram] =:= getType(List(4,5,6))
res3: Boolean = true
scala> typeOf[Histogram] == getType(List(4,5,6))
res4: Boolean = false
As we can see, == incorrectly reports that Histogram and List[Int] have different types.
Querying Types for Members and Declarations
Given a Type, one can also query it for specific members or declarations. A Type’s membersinclude all fields, methods, type aliases, abstract types, nested classes/objects/traits, etc. AType’s declarations are only those members that were declared (not inherited) in the class/trait/object definition which the given Type represents.
To obtain a Symbol for some specific member or declaration, one need only to use methodsmembers or declarations which provide the list of definitions associated with that type. There also exists singular counterparts for each, methods member and declaration as well. The signatures of all four are shown below:
/** The member with given name, either directly declared or inherited, an
* OverloadedSymbol if several exist, NoSymbol if none exist. */
def member(name: Universe.Name): Universe.Symbol
/** The defined or declared members with name name in this type; an
* OverloadedSymbol if several exist, NoSymbol if none exist. */
def declaration(name: Universe.Name): Universe.Symbol
/** A Scope containing all members of this type
* (directly declared or inherited). */
def members: Universe.MemberScope // MemberScope is a type of
// Traversable, use higher-order
// functions such as map,
// filter, foreach to query!
/** A Scope containing the members declared directly on this type. */
def declarations: Universe.MemberScope // MemberScope is a type of
// Traversable, use higher-order
// functions such as map,
// filter, foreach to query!
For example, to look up the map method of List, one can do:
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> typeOf[List[_]].member("map": TermName)
res0: reflect.runtime.universe.Symbol = method map
Note that we pass method member a TermName, since we’re looking up a method. If we were to look up a type member, such as List’s self type, Self, we would pass a TypeName:
scala> typeOf[List[_]].member("Self": TypeName)
res1: reflect.runtime.universe.Symbol = type Self
We can also query all members or declarations on a type in interesting ways. We can use methodmembers to obtain a Traversable (MemberScopeApi extends Traversable) of Symbols representing all inherited or declared members on a given type, which means that we can use popular higher-order functions on collections like foreach, filter, map, etc., to explore our type’s members. For example, to print the members of List which are private, one must simply do:
scala> typeOf[List[Int]].members.filter(_.isPrivate).foreach(println _)
method super$sameElements
method occCounts
class CombinationsItr
class PermutationsItr
method sequential
method iterateUntilEmpty
Trees
Trees are the basis of Scala’s abstract syntax which is used to represent programs. They are also called abstract syntax trees and commonly abbreviated as ASTs.
In Scala reflection, APIs that produce or use trees are the following:
Scala annotations, which use trees to represent their arguments, exposed inAnnotation.scalaArgs (for more, see the Annotations section of this guide).
reify, a special method that takes an expression and returns an AST that represents this expression.
Compile-time reflection with macros (outlined in the Macros guide) and runtime compilation with toolboxes both use trees as their program representation medium.
It’s important to note that trees are immutable except for three fields– pos (Position), symbol(Symbol), and tpe (Type), which are assigned when a tree is typechecked.
Kinds of Trees
There are three main categories of trees:
Subclasses of TermTree which represent terms, e.g., method invocations are represented byApply nodes, object instantiation is achieved using New nodes, etc.
Subclasses of TypTree which represent types that are explicitly specified in program source code, e.g., List[Int] is parsed as AppliedTypeTree. Note: TypTree is not misspelled, nor is it conceptually the same as TypeTree– TypeTree is something different. That is, in situations where Types are constructed by the compiler (e.g., during type inference), they can be wrapped in TypeTree trees and integrated into the AST of the program.
Subclasses of SymTree which introduce or reference definitions. Examples of the introduction of new definitions include ClassDefs which represent class and trait definitions, or ValDefwhich represent field and parameter definitions. Examples of the reference of existing definitions include Idents which refer to an existing definition in the current scope such as a local variable or a method.
Any other type of tree that one might encounter are typically syntactic or short-lived constructs. For example, CaseDef, which wraps individual match cases; such nodes are neither terms nor types, nor do they carry a symbol.
Inspecting Trees
Scala Reflection provides a handful of ways to visualize trees, all available through a universe. Given a tree, one can:
use methods show or toString which print pseudo-Scala code represented by the tree.
use method showRaw to see the raw internal tree that the typechecker operates upon.
For example, given the following tree:
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> val tree = Apply(Select(Ident(newTermName("x")), newTermName("$plus")), List(Literal(Constant(2))))
tree: reflect.runtime.universe.Apply = x.$plus(2)
We can use method show (or toString, which is equivalent) to see what that tree represents.
scala> show(tree)
res0: String = x.$plus(2)
As we can see, tree simply adds 2 to term x.
We can also go in the other direction. Given some Scala expression, we can first obtain a tree, and then use method showRaw to see the raw internal tree that the compiler and typechecker operate on. For example, given the expression:
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> val expr = reify { class Flower { def name = "Rose" } }
expr: reflect.runtime.universe.Expr[Unit] = ...
Here, reify simply takes the Scala expression it was passed, and returns a Scala Expr, which is simply wraps a Tree and a TypeTag (see the Expr section of this guide for more information aboutExprs). We can obtain the tree that expr contains by:
scala> val tree = expr.tree
tree: reflect.runtime.universe.Tree =
{
class Flower extends AnyRef {
def <init>() = {
super.<init>();
()
};
def name = "Rose"
};
()
}
And we can inspect the raw tree by simply doing:
scala> showRaw(tree)
res1: String = Block(List(ClassDef(Modifiers(), newTypeName("Flower"), List(), Template(List(Ident(newTypeName("AnyRef"))), emptyValDef, List(DefDef(Modifiers(), nme.CONSTRUCTOR, List(), List(List()), TypeTree(), Block(List(Apply(Select(Super(This(tpnme.EMPTY), tpnme.EMPTY), nme.CONSTRUCTOR), List())), Literal(Constant(())))), DefDef(Modifiers(), newTermName("name"), List(), List(), TypeTree(), Literal(Constant("Rose"))))))), Literal(Constant(())))
Traversing Trees
After one understands the structure of a given tree, typically the next step is to extract info from it. This is accomplished by traversing the tree, and it can be done in one of two ways:
Traversal via pattern matching.
Using a subclass of Traverser
Traversal via Pattern Matching
Traversal via pattern matching is the simplest and most common way to traverse a tree. Typically, one traverses a tree via pattern matching when they are interested in the state of a given tree at a single node. For example, say we simply want to obtain the function and the argument of the onlyApply node in the following tree:
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> val tree = Apply(Select(Ident(newTermName("x")), newTermName("$plus")), List(Literal(Constant(2))))
tree: reflect.runtime.universe.Apply = x.$plus(2)
We can simply match on our tree, and in the case that we have an Apply node, just returnApply’s function and argument:
scala> val (fun, arg) = tree match {
| case Apply(fn, a :: Nil) => (fn, a)
| }
fun: reflect.runtime.universe.Tree = x.$plus
arg: reflect.runtime.universe.Tree = 2
We can achieve exactly the same thing a bit more concisely, by putting the pattern match on the left-hand side:
scala> val Apply(fun, arg :: Nil) = tree
fun: reflect.runtime.universe.Tree = x.$plus
arg: reflect.runtime.universe.Tree = 2
Note that Trees can typically be quite complex, with nodes nested arbitrarily deep within other nodes. A simple illustration would be if we were to add a second Apply node to the above tree which serves to add 3 to our sum:
scala> val tree = Apply(Select(Apply(Select(Ident(newTermName("x")), newTermName("$plus")), List(Literal(Constant(2)))), newTermName("$plus")), List(Literal(Constant(3))))
tree: reflect.runtime.universe.Apply = x.$plus(2).$plus(3)
If we apply the same pattern match as above, we obtain the outer Apply node which contains as its function the entire tree representing x.$plus(2) that we saw above:
scala> val Apply(fun, arg :: Nil) = tree
fun: reflect.runtime.universe.Tree = x.$plus(2).$plus
arg: reflect.runtime.universe.Tree = 3
scala> showRaw(fun)
res3: String = Select(Apply(Select(Ident(newTermName("x")), newTermName("$plus")), List(Literal(Constant(2)))), newTermName("$plus"))
In cases where one must do some richer task, such as traversing an entire tree without stopping at a specific node, or collecting and inspecting all nodes of a specific type, using Traverser for traversal might be more advantageous.
Traversal via Traverser
In situations where it’s necessary to traverse an entire tree from top to bottom, using traversal via pattern matching would be infeasible– to do it this way, one must individually handle every type of node that we might come across in the pattern match. Thus, in these situations, typically classTraverser is used.
Traverser makes sure to visit every node in a given tree, in a breadth-first search.
To use a Traverser, simply subclass Traverser and override method traverse. In doing so, you can simply provide custom logic to handle only the cases you’re interested in. For example, if, given our x.$plus(2).$plus(3) tree from the previous section, we would like to collect all Applynodes, we could do:
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> val tree = Apply(Select(Apply(Select(Ident(newTermName("x")), newTermName("$plus")), List(Literal(Constant(2)))), newTermName("$plus")), List(Literal(Constant(3))))
tree: reflect.runtime.universe.Apply = x.$plus(2).$plus(3)
scala> object traverser extends Traverser {
| var applies = List[Apply]()
| override def traverse(tree: Tree): Unit = tree match {
| case app @ Apply(fun, args) =>
| applies = app :: applies
| super.traverse(fun)
| super.traverseTrees(args)
| case _ => super.traverse(tree)
| }
| }
defined module traverser
In the above, we intend to construct a list of Apply nodes that we find in our given tree.
We achieve this by in effect adding a special case to the already breadth-first traverse method defined in superclass Traverser, via subclass traverser’s overridden traverse method. Our special case affects only nodes that match the pattern Apply(fun, args), where fun is some function (represented by a Tree) and args is a list of arguments (represented by a list of Trees).
When a the tree matches the pattern (i.e., when we have an Apply node), we simply add it to ourList[Apply], applies, and continue our traversal.
Note that, in our match, we call super.traverse on the function fun wrapped in our Apply, and we call super.traverseTrees on our argument list args (essentially the same assuper.traverse, but for List[Tree] rather than a single Tree). In both of these calls, our objective is simple– we want to make sure that we use the default traverse method in Traverserbecause we don’t know whether the Tree that represents fun contains our Apply pattern– that is, we want to traverse the entire sub-tree. Since the Traverser superclass calls this.traverse, passing in every nested sub- tree, eventually our custom traverse method is guaranteed to be called for each sub-tree that matches our Apply pattern.
To trigger the traverse and to see the resulting List of matching Apply nodes, simply do:
scala> traverser.traverse(tree)
scala> traverser.applies
res0: List[reflect.runtime.universe.Apply] = List(x.$plus(2), x.$plus(2).$plus(3))
Creating Trees
When working with runtime reflection, one need not construct trees manually. However, runtime compilation with toolboxes and compile-time reflection with macros both use trees as their program representation medium. In these cases, there are three recommended ways to create trees:
Via method reify (should be preferred wherever possible).
Via method parse on ToolBoxes.
Manual construction (not recommended).
Tree Creation via reify
Method reify simply takes a Scala expression as an argument, and produces that argument’s typed Tree representation as a result.
Tree creation via method reify is the recommended way of creating trees in Scala Reflection. To see why, let’s start with a small example:
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> { val tree = reify(println(2)).tree; showRaw(tree) }
res0: String = Apply(Select(Select(This(newTypeName("scala")), newTermName("Predef")), newTermName("println")), List(Literal(Constant(2))))
Here, we simply reify the call to println(2)– that is, we convert the expression println(2) to its corresponding tree representation. Then we output the raw tree. Note that the println method was transformed to scala.Predef.println. Such transformations ensure that regardless of where the result of reify is used, it will not unexpectedly change its meaning. For example, even if thisprintln(2) snippet is later inserted into a block of code that defines its own println, it wouldn’t affect the behavior of the snippet.
This way of creating trees is thus hygenic, in the sense that it preserves bindings of identifiers.
Splicing Trees
Using reify also allows one to compose trees from smaller trees. This is done usingExpr.splice.
Note: Expr is reify’s return type. It can be thought of as a simple wrapper which contains a typedTree, a TypeTag and a handful of reification-relevant methods, such as splice. For more information about Exprs, see the relevant section of this guide.
For example, let’s try to construct a tree representing println(2) using splice:
scala> val x = reify(2)
x: reflect.runtime.universe.Expr[Int(2)] = Expr[Int(2)](2)
scala> reify(println(x.splice))
res1: reflect.runtime.universe.Expr[Unit] = Expr[Unit](scala.this.Predef.println(2))
Here, we reify 2 and println separately, and simply splice one into the other.
Note, however, that there is a requirement for the argument of reify to be valid and typeable Scala code. If instead of the argument to println we wanted to abstract over the println itself, it wouldn’t be possible:
scala> val fn = reify(println)
fn: reflect.runtime.universe.Expr[Unit] = Expr[Unit](scala.this.Predef.println())
scala> reify(fn.splice(2))
<console>:12: error: Unit does not take parameters
reify(fn.splice(2))
^
As we can see, the compiler assumes that we wanted to reify a call to println with no arguments, when what we really wanted was to capture the name of the function to be called.
These types of use-cases are currently inexpressible when using reify.
Tree Creation via parse on ToolBoxes
Toolboxes can be used to typecheck, compile, and execute abstract syntax trees. A toolbox can also be used to parse a string into an AST.
Note: Using toolboxes requires scala-compiler.jar to be on the classpath.
Let’s see how parse deals with the println example from the previous section:
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> import scala.tools.reflect.ToolBox
import scala.tools.reflect.ToolBox
scala> val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
tb: scala.tools.reflect.ToolBox[reflect.runtime.universe.type] = scala.tools.reflect.ToolBoxFactory$ToolBoxImpl@7bc979dd
scala> showRaw(tb.parse("println(2)"))
res2: String = Apply(Ident(newTermName("println")), List(Literal(Constant(2))))
It’s important to note that, unlike reify, toolboxes aren’t limited by the typeability requirement– although this flexibility is achieved by sacrificing robustness. That is, here we can see that parse, unlike reify, doesn’t reflect the fact that println should be bound to the standard printlnmethod.
Note: when using macros, one shouldn’t use ToolBox.parse. This is because there’s already aparse method built into the macro context. For example:
scala> import language.experimental.macros
import language.experimental.macros
scala> def impl(c: scala.reflect.macros.Context) = c.Expr[Unit](c.parse("println(2)"))
impl: (c: scala.reflect.macros.Context)c.Expr[Unit]
scala> def test = macro impl
test: Unit
scala> test
2
Typechecking with ToolBoxes
As earlier alluded to, ToolBoxes enable one to do more than just constructing trees from strings. They can also be used to typecheck, compile, and execute trees.
In addition to outlining the structure of the program, trees also hold important information about the semantics of the program encoded in symbol (a symbol assigned to trees that introduce or reference definitions), and tpe (the type of the tree). By default these fields are empty, but typechecking fills them in.
When using the runtime reflection framework, typechecking is implemented byToolBox.typeCheck. When using macros, at compile time one can use the Context.typeCheckmethod.
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> val tree = reify { "test".length }.tree
tree: reflect.runtime.universe.Tree = "test".length()
scala> import scala.tools.reflect.ToolBox
import scala.tools.reflect.ToolBox
scala> val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
tb: scala.tools.reflect.ToolBox[reflect.runtime.universe.type] = ...
scala> val ttree = tb.typeCheck(tree)
ttree: tb.u.Tree = "test".length()
scala> ttree.tpe
res5: tb.u.Type = Int
scala> ttree.symbol
res6: tb.u.Symbol = method length
Here, we simply create a tree that represents a call to "test".length, and use ToolBox tb’stypeCheck method to typecheck the tree. As we can see, ttree gets the correct type, Int, and its Symbol is correctly set.
Tree Creation via Manual Construction
If all else fails, one can manually construct trees. This is the most low-level way to create trees, and it should only be attempted if no other approach works. It sometimes offers greater flexibility when compared with parse, though this flexibility is achieved at a cost of excessive verbosity and fragility.
Our earlier example involving println(2) can be manually constructed as follows:
scala> Apply(Ident(newTermName("println")), List(Literal(Constant(2))))
res0: reflect.runtime.universe.Apply = println(2)
The canonical use case for this technique is when the target tree needs to be assembled from dynamically created parts, which don’t make sense in isolation from one another. In that case,reify will most likely be inapplicable, because it requires its argument to be typeable. parsemight not work either, since quite often, trees are assembled on sub-expression level, with individual parts being inexpressible as Scala sources.
posted @ 2014-12-22 15:56 Scan. 阅读(...) 评论(...) 编辑 收藏
刷新评论刷新页面返回顶部
公告
Copyright ©2016 Scan.
上一篇: 站长赚钱:开启网站优化创业之路