Movatterモバイル変換


[0]ホーム

URL:


Reflection

Symbols, Trees, and Types

    Language
      This doc page is specific to features shipped in Scala 2, which have either been removed in Scala 3 or replaced by an alternative. Unless otherwise stated, all the code examples in this page assume you are using Scala 2.

      EXPERIMENTAL

      Symbols

      Symbols are used to establish bindings between a name and the entity it refersto, such as a class or a method. Anything you define and can give a name to inScala has an associated symbol.

      Symbols contain all available information about the declaration of an entity(class/object/trait etc.) or a member (vals/vars/defs etc.), andas such are an integral abstraction central to both runtime reflection andcompile-time reflection (macros).

      A symbol can provide a wealth of information ranging from the basicnamemethod available on all symbols to other, more involved, concepts such asgetting thebaseClasses fromClassSymbol. Other common use cases ofsymbols include inspecting members’ signatures, getting type parameters of aclass, getting the parameter type of a method or finding out the type of afield.

      The Symbol Owner Hierarchy

      Symbols are organized in a hierarchy. For example, a symbol that represents aparameter of a method isowned by the corresponding method symbol, a methodsymbol isowned by its enclosing class, trait, or object, a class isownedby a containing package and so on.

      If a symbol does not have an owner, for example, because it refers to a top-levelentity, such as a top-level package, then its owner is the specialNoSymbol singleton object. Representing a missing symbol,NoSymbol iscommonly used in the API to denote an empty or default value. Accessing theowner ofNoSymbol throws an exception. See the API docs for the generalinterface provided by typeSymbol

      TypeSymbols

      ATypeSymbol represents type, class, and trait declarations, as well as typeparameters. Interesting members that do not apply to the more specificClassSymbols, includeisAbstractType,isContravariant, andisCovariant.

      • ClassSymbol: Provides access to all information contained in a class or trait declaration, e.g.,name, modifiers (isFinal,isPrivate,isProtected,isAbstractClass, etc.),baseClasses, andtypeParams.

      TermSymbols

      The type of term symbols representing val, var, def, and object declarationsas 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 membermoduleClass. The opposite look up is also possible. One can go back from a module class to the associated module symbol by inspecting itsselfType.termSymbol.

      Symbol Conversions

      There can be situations where one uses a method that returns an instance ofthe generalSymbol type. In cases like these, it’s possible to convert themore generalSymbol type obtained to the specific, more specialized symboltype needed.

      Symbol conversions, such asasClass orasMethod, are used to convert to amore specific subtype ofSymbol as appropriate (if you want to use theMethodSymbol interface, for example).

      For example,

      scala> import scala.reflect.runtime.universe._import scala.reflect.runtime.universe._scala> class C[T] { def test[U](x: T)(y: U): Int = ??? }defined class Cscala> val testMember = typeOf[C[Int]].member(TermName("test"))testMember: scala.reflect.runtime.universe.Symbol = method test

      In this case,member returns an instance ofSymbol, notMethodSymbol asone might expect. Thus, we must useasMethod to ensure that we obtain aMethodSymbol

      scala> testMember.asMethodres0: scala.reflect.runtime.universe.MethodSymbol = method test

      Free symbols

      The two symbol typesFreeTermSymbol andFreeTypeSymbol have a specialstatus, in the sense that they refer to symbols whose available information isnot 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 notavailable in the corresponding class file, for example, because the symbolrefers to a local class), it reifies it as a so-called “free type”, asynthetic dummy symbol that remembers the original name and owner and has asurrogate type signature that closely follows the original. You can checkwhether a symbol is a free type by callingsym.isFreeType. You can also geta list of all free types referenced by a tree and its children by callingtree.freeTypes. Finally, you can get warnings when reification produces freetypes by using-Xlog-free-types.

      Types

      As its name suggests, instances ofType represent information about the typeof a corresponding symbol. This includes its members (methods, fields, typealiases, abstract types, nested classes, traits, etc.) either declareddirectly or inherited, its base types, its erasure and so on. Types alsoprovide operations to test for type conformance or equivalence.

      Instantiating Types

      In general, there are three ways to instantiate aType.

      1. via methodtypeOf onscala.reflect.api.TypeTags, which is mixed intoUniverse (simplest and most common).
      2. Standard Types, such asInt,Boolean,Any, orUnit are accessible through the available universe.
      3. Manual instantiation using factory methods such astypeRef orpolyType onscala.reflect.api.Types, (not recommended).

      Instantiating Types WithtypeOf

      To instantiate a type, most of the time, thescala.reflect.api.TypeTags#typeOf method can be used. It takes a typeargument and produces aType instance which represents that argument. Forexample:

      scala> import scala.reflect.runtime.universe._import scala.reflect.runtime.universe._scala> typeOf[List[Int]]res0: scala.reflect.runtime.universe.Type = scala.List[Int]

      In this example, ascala.reflect.api.Types$TypeRefis returned, which corresponds to the type constructorList, applied tothe type argumentInt.

      Note, however, that this approach requires one to specify by hand the typewe’re trying to instantiate. What if we’re interested in obtaining an instanceofType that corresponds to some arbitrary instance? One can simply define amethod with a context bound on the type parameter– this generates aspecializedTypeTag for us, which we can use to obtain the type of ourarbitrary instance:

      scala> def getType[T: TypeTag](obj: T) = typeOf[T]getType: [T](obj: T)(implicit evidence$1: scala.reflect.runtime.universe.TypeTag[T])scala.reflect.runtime.universe.Typescala> getType(List(1,2,3))res1: scala.reflect.runtime.universe.Type = List[Int]scala> class Animal; class Cat extends Animaldefined class Animaldefined class Catscala> val a = new Animala: Animal = Animal@21c17f5ascala> getType(a)res2: scala.reflect.runtime.universe.Type = Animalscala> val c = new Catc: Cat = Cat@2302d72dscala> getType(c)res3: scala.reflect.runtime.universe.Type = Cat

      Note: MethodtypeOf does not work for types with type parameters, such astypeOf[List[A]] whereA is a type parameter. In this case, one can usescala.reflect.api.TypeTags#weakTypeOf instead. For more details, see theTypeTagssection of this guide.

      Standard Types

      Standard types, such asInt,Boolean,Any, orUnit, are accessible through a universe’sdefinitions member. For example:

      scala> import scala.reflect.runtime.universeimport scala.reflect.runtime.universescala> val intTpe = universe.definitions.IntTpeintTpe: scala.reflect.runtime.universe.Type = Int

      The list of standard types is specified in traitStandardTypes 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:

      1. Checking the subtyping relationship between two types.
      2. Checking for equality between two types.
      3. Querying a given type for certain members or inner types.

      Subtyping Relationships

      Given twoType instances, one can easily test whether one is a subtype ofthe other using<:< (and in exceptional cases,weak_<:<, explained below)

      scala> import scala.reflect.runtime.universe._import scala.reflect.runtime.universe._scala> class A; class B extends Adefined class Adefined class Bscala> typeOf[A] <:< typeOf[B]res0: Boolean = falsescala> typeOf[B] <:< typeOf[A]res1: Boolean = true

      Note that methodweak_<:< exists to check forweak conformance between twotypes. 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<:wShort
      Short<:wInt
      Char<:wInt
      Int<:wLong
      Long<:wFloat
      Float<:wDouble

      For example, weak conformance is used to determine the type of the following if-expression:

      scala> if (true) 1 else 1dres2: Double = 1.0

      In the if-expression shown above, the result type is defined to be theweak least upper bound of the two types (i.e., the least upper bound withrespect to weak conformance).

      Thus, sinceDouble is defined to be the least upper bound with respect toweak conformance betweenInt andDouble (according to the spec, shownabove),Double is inferred as the type of our example if-expression.

      Note that methodweak_<:< checks forweak conformance (as opposed to<:<which checks for conformance without taking into consideration weakconformance relations in section 3.5.3 of the spec) and thus returns thecorrect result when inspecting conformance relations between numeric typesInt andDouble:

      scala> typeOf[Int] weak_<:< typeOf[Double]res3: Boolean = truescala> typeOf[Double] weak_<:< typeOf[Int]res4: Boolean = false

      Whereas using<:< would incorrectly report thatInt andDouble do notconform to each other in any way:

      scala> typeOf[Int] <:< typeOf[Double]res5: Boolean = falsescala> typeOf[Double] <:< typeOf[Int]res6: Boolean = false

      Type Equality

      Similar to type conformance, one can easily check theequality of two types.That is, given two arbitrary types, one can use method=:= to see if bothdenote 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: scala.reflect.runtime.universe.TypeTag[T])scala.reflect.runtime.universe.Typescala> class Adefined class Ascala> val a1 = new A; val a2 = new Aa1: A = A@cddb2e7a2: A = A@2f0c624ascala> getType(a1) =:= getType(a2)res0: Boolean = true

      Note that theprecise type info must be the same for both instances. In thefollowing code snippet, for example, we have two instances ofListwith different type arguments.

      scala> getType(List(1,2,3)) =:= getType(List(1.0, 2.0, 3.0))res1: Boolean = falsescala> getType(List(1,2,3)) =:= getType(List(9,8,7))res2: Boolean = true

      Also important to note is that=:= shouldalways be used to compare typesfor equality. That is, never use==, as it can’t check for type equality inthe presence of type aliases, whereas=:= can:

      scala> type Histogram = List[Int]defined type alias Histogramscala> typeOf[Histogram] =:= getType(List(4,5,6))res3: Boolean = truescala> typeOf[Histogram] == getType(List(4,5,6))res4: Boolean = false

      As we can see,== incorrectly reports thatHistogram andList[Int] havedifferent types.

      Querying Types for Members and Declarations

      Given aType, one can alsoquery it for specific members or declarations.AType’smembers include all fields, methods, type aliases, abstracttypes, nested classes/objects/traits, etc. AType’sdeclarations are onlythose members that were declared (not inherited) in the class/trait/objectdefinition which the givenType represents.

      To obtain aSymbol for some specific member or declaration, one need only to use methodsmembers ordecls which provide the list of definitions associated with that type. There also exists singular counterparts for each, methodsmember anddecl 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 decl(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 decls: Universe.MemberScope // MemberScope is a type of                                       // Traversable, use higher-order                                       // functions such as map,                                       // filter, foreach to query!

      For example, to look up themap method ofList, one can do:

      scala> import scala.reflect.runtime.universe._import scala.reflect.runtime.universe._scala> typeOf[List[_]].member(TermName("map"))res0: scala.reflect.runtime.universe.Symbol = method map

      Note that we pass methodmember aTermName, since we’re looking up amethod. If we were to look up a type member, such asList’s self type,Self, wewould pass aTypeName:

      scala> typeOf[List[_]].member(TypeName("Self"))res1: scala.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 aTraversable (MemberScopeApiextendsTraversable) ofSymbols representing all inherited or declaredmembers on a given type, which means that we can use popular higher-orderfunctions on collections likeforeach,filter,map, etc., to explore ourtype’s members. For example, to print the members ofList which are private,one must simply do:

      scala> typeOf[List[Int]].members.filter(_.isPrivate).foreach(println _)method super$sameElementsmethod occCountsclass CombinationsItrclass PermutationsItrmethod sequentialmethod iterateUntilEmpty

      Trees

      Trees are the basis of Scala’s abstract syntax which is used to representprograms. They are also called abstract syntax trees and commonly abbreviatedas ASTs.

      In Scala reflection, APIs that produce or use trees are the following:

      1. Scala annotations, which use trees to represent their arguments, exposed inAnnotation.scalaArgs (for more, see theAnnotations section of this guide).
      2. reify, a special method that takes an expression and returns an AST that represents this expression.
      3. Compile-time reflection with macros (outlined in theMacros 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), andtpe (Type), which areassigned when a tree is typechecked.

      Kinds ofTrees

      There are three main categories of trees:

      1. Subclasses ofTermTree which represent terms,e.g., method invocations are represented byApply nodes, object instantiation is achieved usingNew nodes, etc.
      2. Subclasses ofTypTree which represent types that are explicitly specified in program source code,e.g.,List[Int] is parsed asAppliedTypeTree.Note:TypTree is not misspelled, nor is it conceptually the same asTypeTreeTypeTree is something different. That is, in situations whereTypes are constructed by the compiler (e.g., during type inference), they can be wrapped inTypeTree trees and integrated into the AST of the program.
      3. Subclasses ofSymTree which introduce or reference definitions. Examples of the introduction of new definitions includeClassDefs which represent class and trait definitions, orValDef which represent field and parameter definitions. Examples of the reference of existing definitions includeIdents 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 orshort-lived constructs. For example,CaseDef, which wraps individual matchcases; 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 availablethrough a universe. Given a tree, one can:

      • use methodsshow ortoString which print pseudo-Scala code represented by the tree.
      • use methodshowRaw 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(TermName("x")), TermName("$plus")), List(Literal(Constant(2))))tree: scala.reflect.runtime.universe.Apply = x.$plus(2)

      We can use methodshow (ortoString, which is equivalent) to see what thattree represents.

      scala> show(tree)res0: String = x.$plus(2)

      As we can see,tree simply adds2 to termx.

      We can also go in the other direction. Given some Scala expression, we canfirst obtain a tree, and then use methodshowRaw to see the raw internaltree that the compiler and typechecker operate on. For example, given theexpression:

      scala> import scala.reflect.runtime.universe._import scala.reflect.runtime.universe._scala> val expr = reify { class Flower { def name = "Rose" } }expr: scala.reflect.runtime.universe.Expr[Unit] = ...

      Here,reify simply takes the Scala expression it was passed, and returns aScalaExpr, which is simply wraps aTree and aTypeTag (see theExprsection of this guide for more information aboutExprs). We can obtainthe tree thatexpr contains by:

      scala> val tree = expr.treetree: scala.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(), TypeName("Flower"), List(), Template(List(Ident(TypeName("AnyRef"))), emptyValDef, List(DefDef(Modifiers(), termNames.CONSTRUCTOR, List(), List(List()), TypeTree(), Block(List(Apply(Select(Super(This(typeNames.EMPTY), typeNames.EMPTY), termNames.CONSTRUCTOR), List())), Literal(Constant(())))), DefDef(Modifiers(), TermName("name"), List(), List(), TypeTree(), Literal(Constant("Rose"))))))), Literal(Constant(())))

      Traversing Trees

      After one understands the structure of a given tree, typically the next stepis to extract info from it. This is accomplished bytraversing the tree, andit can be done in one of two ways:

      • Traversal via pattern matching.
      • Using a subclass ofTraverser

      Traversal via Pattern Matching

      Traversal via pattern matching is the simplest and most common way to traversea tree. Typically, one traverses a tree via pattern matching when they areinterested in the state of a given tree at a single node. For example, say wesimply want to obtain the function and the argument of the onlyApply nodein the following tree:

      scala> import scala.reflect.runtime.universe._import scala.reflect.runtime.universe._scala> val tree = Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2))))tree: scala.reflect.runtime.universe.Apply = x.$plus(2)

      We can simply match on ourtree, and in the case that we have anApplynode, just returnApply’s function and argument:

      scala> val (fun, arg) = tree match {     |     case Apply(fn, a :: Nil) => (fn, a)     | }fun: scala.reflect.runtime.universe.Tree = x.$plusarg: scala.reflect.runtime.universe.Tree = 2

      We can achieve exactly the same thing a bit more concisely, by putting thepattern match on the left-hand side:

      scala> val Apply(fun, arg :: Nil) = treefun: scala.reflect.runtime.universe.Tree = x.$plusarg: scala.reflect.runtime.universe.Tree = 2

      Note thatTrees can typically be quite complex, with nodes nestedarbitrarily deep within other nodes. A simple illustration would be if we wereto add a secondApply node to the above tree which serves to add3 to oursum:

      scala> val tree = Apply(Select(Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2)))), TermName("$plus")), List(Literal(Constant(3))))tree: scala.reflect.runtime.universe.Apply = x.$plus(2).$plus(3)

      If we apply the same pattern match as above, we obtain the outerApply nodewhich contains as its function the entire tree representingx.$plus(2) thatwe saw above:

      scala> val Apply(fun, arg :: Nil) = treefun: scala.reflect.runtime.universe.Tree = x.$plus(2).$plusarg: scala.reflect.runtime.universe.Tree = 3scala> showRaw(fun)res3: String = Select(Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2)))), TermName("$plus"))

      In cases where one must do some richer task, such as traversing an entiretree without stopping at a specific node, or collecting and inspecting allnodes of a specific type, usingTraverser for traversal might be moreadvantageous.

      Traversal viaTraverser

      In situations where it’s necessary to traverse an entire tree from top tobottom, using traversal via pattern matching would be infeasible– to do itthis way, one must individually handle every type of node that we might comeacross 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 depth-first search.

      To use aTraverser, simply subclassTraverser and override methodtraverse. In doing so, you can simply provide custom logic to handle onlythe cases you’re interested in. For example, if, given ourx.$plus(2).$plus(3) tree from the previous section, we would like to collectallApply nodes, we could do:

      scala> import scala.reflect.runtime.universe._import scala.reflect.runtime.universe._scala> val tree = Apply(Select(Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2)))), TermName("$plus")), List(Literal(Constant(3))))tree: scala.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 ofApply nodes that we find inour given tree.

      We achieve this by in effectadding a special case to the already depth-firsttraverse method defined in superclassTraverser, via subclasstraverser’s overriddentraverse method. Our special case affects onlynodes that match the patternApply(fun, args), wherefun is some function(represented by aTree) andargs is a list of arguments (represented by alist ofTrees).

      When a tree matches the pattern (i.e., when we have anApply node), wesimply add it to ourList[Apply],applies, and continue our traversal.

      Note that, in our match, we callsuper.traverse on the functionfunwrapped in ourApply, and we callsuper.traverseTrees on our argument listargs (essentially the same assuper.traverse, but forList[Tree] ratherthan a singleTree). In both of these calls, our objective is simple– wewant to make sure that we use the defaulttraverse method inTraverserbecause we don’t know whether theTree that represents fun contains ourApply pattern– that is, we want to traverse the entire sub-tree. Since theTraverser superclass callsthis.traverse, passing in every nested sub-tree, eventually our customtraverse method is guaranteed to be called foreach sub-tree that matches ourApply pattern.

      To trigger thetraverse and to see the resultingList of matchingApplynodes, simply do:

      scala> traverser.traverse(tree)scala> traverser.appliesres0: List[scala.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 withmacros both use trees as their program representation medium. In these cases,there are three recommended ways to create trees:

      1. Via methodreify (should be preferred wherever possible).
      2. Via methodparse onToolBoxes.
      3. Manual construction (not recommended).

      Tree Creation viareify

      Methodreify simply takes a Scala expression as an argument, and producesthat argument’s typedTree representation as a result.

      Tree creation via methodreify is the recommended way of creating trees inScala 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(TypeName("scala")), TermName("Predef")), TermName("println")), List(Literal(Constant(2))))

      Here, we simplyreify the call toprintln(2)– that is, we convert theexpressionprintln(2) to its corresponding tree representation. Then weoutput the raw tree. Note that theprintln method was transformed toscala.Predef.println. Such transformations ensure that regardless of wherethe result ofreify is used, it will not unexpectedly change its meaning.For example, even if thisprintln(2) snippet is later inserted into a blockof code that defines its ownprintln, it wouldn’t affect the behavior of thesnippet.

      This way of creating trees is thushygenic, in the sense that it preservesbindings of identifiers.

      Splicing Trees

      Usingreify also allows one to compose trees from smaller trees. This isdone usingExpr.splice.

      Note:Expr isreify’s return type. It can be thought of as a simplewrapper which contains atypedTree, aTypeTag and a handful ofreification-relevant methods, such assplice. For more information aboutExprs, seethe relevant section of this guide.

      For example, let’s try to construct a tree representingprintln(2) usingsplice:

      scala> val x = reify(2)x: scala.reflect.runtime.universe.Expr[Int(2)] = Expr[Int(2)](2)scala> reify(println(x.splice))res1: scala.reflect.runtime.universe.Expr[Unit] = Expr[Unit](scala.this.Predef.println(2))

      Here, wereify2 andprintln separately, and simplysplice one intothe other.

      Note, however, that there is a requirement for the argument ofreify to bevalid and typeable Scala code. If instead of the argument toprintln wewanted to abstract over theprintln itself, it wouldn’t be possible:

      scala> val fn = reify(println)fn: scala.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 toprintln with no arguments, when what we really wanted was to capture thename of the function to be called.

      These types of use-cases are currently inexpressible when usingreify.

      Tree Creation viaparse onToolBoxes

      Toolboxes can be used to typecheck, compile, and execute abstract syntaxtrees. A toolbox can also be used to parse a string into an AST.

      Note: Using toolboxes requiresscala-compiler.jar to be on the classpath.

      Let’s see howparse deals with theprintln example from the previoussection:

      scala> import scala.reflect.runtime.universe._import scala.reflect.runtime.universe._scala> import scala.tools.reflect.ToolBoximport scala.tools.reflect.ToolBoxscala> val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()tb: scala.tools.reflect.ToolBox[scala.reflect.runtime.universe.type] = scala.tools.reflect.ToolBoxFactory$ToolBoxImpl@7bc979ddscala> showRaw(tb.parse("println(2)"))res2: String = Apply(Ident(TermName("println")), List(Literal(Constant(2))))

      It’s important to note that, unlikereify, toolboxes aren’t limited by thetypeability requirement– although this flexibility is achieved by sacrificingrobustness. That is, here we can see thatparse, unlikereify, doesn’treflect the fact thatprintln should be bound to the standardprintlnmethod.

      Note: when using macros, one shouldn’t useToolBox.parse. This is becausethere’s already aparse method built into the macro context. For example:

      bash$ scala -Yrepl-class-based:falsescala> import scala.language.experimental.macrosimport scala.language.experimental.macrosscala> def impl(c: scala.reflect.macros.whitebox.Context) = c.Expr[Unit](c.parse("println(2)"))def impl(c: scala.reflect.macros.whitebox.Context): c.Expr[Unit]scala> def test: Unit = macro impldef test: Unitscala> test2

      You can find more about the twoContexts inthis Macros article.

      Typechecking with ToolBoxes

      As earlier alluded to,ToolBoxes enable one to do more than justconstructing 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 holdimportant information about the semantics of the program encoded insymbol(a symbol assigned to trees that introduce or reference definitions), andtpe (the type of the tree). By default, these fields are empty, buttypechecking fills them in.

      When using the runtime reflection framework, typechecking is implemented byToolBox.typeCheck. When using macros, at compile time one can use theContext.typeCheck method.

      scala> import scala.reflect.runtime.universe._import scala.reflect.runtime.universe._scala> val tree = reify { "test".length }.treetree: scala.reflect.runtime.universe.Tree = "test".length()scala> import scala.tools.reflect.ToolBoximport scala.tools.reflect.ToolBoxscala> val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()tb: scala.tools.reflect.ToolBox[scala.reflect.runtime.universe.type] = ...scala> val ttree = tb.typeCheck(tree)ttree: tb.u.Tree = "test".length()scala> ttree.tperes5: tb.u.Type = Intscala> ttree.symbolres6: tb.u.Symbol = method length

      Here, we simply create a tree that represents a call to"test".length, anduseToolBoxtb’stypeCheck method to typecheck the tree. As we can see,ttree gets the correct type,Int, and itsSymbol is correctly set.

      Tree Creation via Manual Construction

      If all else fails, one can manually construct trees. This is the most low-levelway to create trees, and it should only be attempted if no otherapproach works. It sometimes offers greater flexibility when compared withparse, though this flexibility is achieved at a cost of excessive verbosityand fragility.

      Our earlier example involvingprintln(2) can be manually constructed asfollows:

      scala> Apply(Ident(TermName("println")), List(Literal(Constant(2))))res0: scala.reflect.runtime.universe.Apply = println(2)

      The canonical use case for this technique is when the target tree needs to beassembled from dynamically created parts, which don’t make sense in isolationfrom one another. In that case,reify will most likely be inapplicable,because it requires its argument to be typeable.parse might not workeither, since quite often, trees are assembled on sub-expression level, withindividual parts being inexpressible as Scala sources.

      Contributors to this page:

      Contents


      [8]ページ先頭

      ©2009-2025 Movatter.jp