Symbols, Trees, and Types
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 (val
s/var
s/def
s 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 basicname
method 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
TypeSymbol
s
ATypeSymbol
represents type, class, and trait declarations, as well as typeparameters. Interesting members that do not apply to the more specificClassSymbol
s, 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
.
TermSymbol
s
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
.
- via method
typeOf
onscala.reflect.api.TypeTags
, which is mixed intoUniverse
(simplest and most common). - Standard Types, such as
Int
,Boolean
,Any
, orUnit
are accessible through the available universe. - Manual instantiation using factory methods such as
typeRef
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$TypeRef
is 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:
- 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 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 <: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 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 ofList
with 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
(MemberScopeApi
extendsTraversable
) ofSymbol
s 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:
- Scala annotations, which use trees to represent their arguments, exposed in
Annotation.scalaArgs
(for more, see theAnnotations 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 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 ofTree
s
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 usingNew
nodes, etc. - Subclasses of
TypTree
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 asTypeTree
–TypeTree
is something different. That is, in situations whereType
s are constructed by the compiler (e.g., during type inference), they can be wrapped inTypeTree
trees and integrated into the AST of the program. - Subclasses of
SymTree
which introduce or reference definitions. Examples of the introduction of new definitions includeClassDef
s which represent class and trait definitions, orValDef
which represent field and parameter definitions. Examples of the reference of existing definitions includeIdent
s 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 methods
show
ortoString
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(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 aboutExpr
s). 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 of
Traverser
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 anApply
node, 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 thatTree
s 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 ofTree
s).
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 functionfun
wrapped 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 inTraverser
because 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 matchingApply
nodes, 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:
- Via method
reify
(should be preferred wherever possible). - Via method
parse
onToolBox
es. - 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 aboutExpr
s, 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, wereify
2
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
onToolBox
es
Toolbox
es 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 standardprintln
method.
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 twoContext
s inthis Macros article.
Typechecking with ToolBoxes
As earlier alluded to,ToolBox
es 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
, anduseToolBox
tb
’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.