This document summarizes a presentation about practical type mining in Scala using reflection. It discusses how Salat used pickled Scala signatures before 2.10 to mine types without runtime reflection. It covers benefits of 2.10 reflection like choosing runtime vs compile-time reflection. It provides an overview of universes, mirrors, symbols and trees, and how to navigate and inspect them. It also discusses limitations like non-thread-safe reflection and provides examples of macros libraries.
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Practical type mining in Scala
1. Practical type mining in Scala
the fastest way from A to Z.tpe
Rose Toomey, Novus Partners
11 June 2013 @ Scala Days
2. Where Salat started
https://github.com/novus/salat
Salat began because we wanted a
simple, seamless way to serialize and
deserialize our data model without external
mappings.
Pickled Scala signatures (SID #10) allowed us
to mine type information without resorting to
runtime reflection.
3. Scala reflection before 2.10
Why did Salat resort to pickled Scala signatures?
• Scala before 2.10 used reflection from Java
– Reflection didn’t know about Scala features like
implicits, path-dependent types, etc.
– Type erasure: parameterized types were
unrecoverable at runtime without Manifest
workaround
Why should we settle for having less information than the
compiler does?
Workaround: raid the compiler’s secret stash.
4. Benefits of Scala 2.10 reflection
• Choose between runtime and compile time
reflection
• Significant parts of the compiler API are now exposed
• Reify Scala expressions into abstract syntax trees
• Vastly better documentation!
5. Navigating the universe
A universe is an environment with
access to trees, symbols and their
types.
• scala.reflect.runtime.universe
links symbols and types to the
underlying classes and runtime
values of the JVM
• scala.reflect.macros.Universe
is the compiler universe
6. Macros and the compiler
The compiler universe has one mirror
scala.tools.nsc.Global#rootMirror
Macros access the compiler universe and
mirror via an instance of
scala.reflect.macros.Context
To get started with a simple example, see
Eugene Burmako’s printf macro:
http://docs.scala-
lang.org/overviews/macros/overview.html
7. Mirror, mirror
Mirrors provide access to the symbol
table within a universe.
The compiler has one universe and one
mirror, which loads symbols from pickled
Scala signatures using ClassFileParser.
At runtime there is only one universe, but
it has a mirror for each classloader. The
classloader mirror creates invoker
mirrors, which are used for
instances, classes, methods, fields –
everything.
8. Which universe?
Play with the compiler’s universe using the Scala REPL :power mode.
At runtime, get a mirror for your classloader and then use
reflect, reflectClass and reflectModule to get more specific invoker
mirrors.
scala.reflect.runtime.currentMirror
For macros, your macro implementation takes a Context c and then import
the macro universe.
The macro universe exposes the compiler universe and provides mutability
for reflection artifacts so your macros can create or transform ASTs.
import c.universe._
9. Symbols and Types
Symbols exist in a hierarchy that
provides all available information
about the declaration of entities and
members.
Types represent information about
the type of a symbol: its
members, base
types, erasure, modifiers, etc.
10. What can I do with a Type?
• Comparisons: check equality, subtyping
• Mine type information about the members and inner types
– declarations gets all the members declared on the type
– members gets all the members of this type, either declared or
inherited
– Use declaration or member to find a type by symbol
Get the type’s own termSymbol or typeSymbol
Type instances represent information about the type of a
corresponding symbol – so to understand types we need to
examine which types of symbols are interesting and why.
11. Great! Now I want a type…
Import a universe and use typeOf:
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> case class Foo(x: Int)
defined class Foo
scala> val fooTpe = typeOf[Foo]
fooTpe: reflect.runtime.universe.Type = Foo
scala.reflect.internal.Definitions defines value class
types (Unit, primitives) and trivial types
(Any, AnyVal, AnyRef).
12. Comparing types
Don’t compare types using == to check for
equality because under certain conditions it does
not work. Type aliases are one example but due
to some internal implementation details, == could
fail even for the same types if they were loaded
differently.
Use these handy emoji instead:
=:= Is this type equal to that type?
<:< Is this type a subtype of that type?
* Don’t confuse these type comparisons with deprecated Manifest operations like <:< and >:>
* Tip of the hat to @softprops for the original emoji usage in his presentation on sbt
13. Inspecting types in detail
The REPL :power mode is full of undocumented treats like :type –v
scala> :type -v case class Foo[T](t: T)
// Type signature
[T]AnyRef
with Product
with Serializable {
val t: T
private[this] val t: T
def <init>(t: T): Foo[T]
def copy[T](t: T): Foo[T]
...
}
// Internal Type structure
PolyType(
typeParams = List(TypeParam(T))
resultType = ClassInfoType(
...
)
)
14. Symbols in more depth
Start here:
scala.reflect.internal.Symbols
TypeSymbol represents types, classes, traits and type parameters.
It provides information about covariance and contravariance.
TermSymbol covers a lot of ground: var, val, def, object
declarations.
SymbolApi provides is methods to check whether a Symbol
instance can be cast to a more specific type of symbol, as well as
as methods to actually cast, e.g. isTerm and asTerm.
15. Interesting type symbols
ClassSymbol provides access to all the information
contained in a class or trait.
• baseClasses in linear order from most to least specific
• isAbstractClass, isTrait, isCaseClass
• isNumeric, isPrimitive, isPrimitiveValueClass
• Find companion objects
16. The world of term symbols
Term symbols represent val, var, def, and object
declarations as well as packages and value parameters.
Accordingly you can find interesting methods on them like:
• isVal, isVar
• isGetter, isSetter, isAccessor, isParamAccessor
• isParamWithDefault (note there is not any easy way to get
the value of the default argument yet)
• isByNameParam (big improvement!)
• isLazy
17. Term symbols: methods
Use MethodSymbol to get all the details of methods:
• is it a constructor? the primary constructor?
• use paramss to get all the parameter lists of the methods
(ss = list of lists of symbols)
• return type
• type params (empty for non parameterized methods)
• does the method support variable length argument lists?
When members or member(ru.Name) returns a Symbol, you can
convert it to a MethodSymbol using asMethod
18. Term symbols: modules
Use ModuleSymbol to navigate object declarations:
• Find companion objects (See this StackOverflow
discussion)
• Find nested objects (See this StackOverflow
discussion)
Given a ClassSymbol, use companionSymbol.asModule to get a
ModuleSymbol which you can turn into a companion
object instance using the mirror
reflectModule(moduleSymbol).instance
19. Getting symbols out of types
Have a Type?
- typeSymbol returns either NoSymbol or a Symbol which can
be cast using asType
- similarly, termSymbol
Use the members method to get a MemberScope, which has an
iterator of symbols:
scala> typeOf[Foo].members
res61: reflect.runtime.universe.MemberScope =
Scopes(constructor Foo, value x, ...
20. Ask for it by name
If you know exactly what you want, use newTermName and
newTypeName. If it doesn’t work out, you’ll get back NoSymbol.
scala> case class Foo(x: Int)
defined class Foo
scala> typeOf[Foo].member(ru.newTermName("x"))
res64: reflect.runtime.universe.Symbol = value x
scala> typeOf[Foo].member(ru.newTypeName("x"))
res65: reflect.runtime.universe.Symbol = <none>
21. Find the constructor
scala.reflect.api.StandardNames provides standard term
names as nme, available from your universe.
scala> typeOf[Foo].member(nme.CONSTRUCTOR)
res66: reflect.runtime.universe.Symbol =
constructor Foo
scala> res66.asMethod.isPrimaryConstructor
res68: Boolean = true
22. Trees
Trees (ASTs) are the foundation of
Scala’s abstract type syntax for
representing code.
The parser creates an untyped tree
structure that is immutable except for
Position, Symbol and Type. A later
stage of the compiler then fills in this
information.
23. From tree to Scala signature
$ scalac -Xshow-phases
phase name id description
---------- -- -----------
parser 1 parse source into ASTs, perform simple
desugaring
namer 2 resolve names, attach symbols to named trees
typer 4 the meat and potatoes: type the trees
pickler 8 serialize symbol tables
• The parser creates trees
• The namer fills in tree symbols, creates completers (symbol.info)
• The typer computes types for trees
• The pickler serializes symbols along with types into ScalaSignature
annotation
24. Make it so
reify takes a Scala expression and converts
into into a tree.
When you use reify to create a tree, it is
hygienic: once the identifiers in the tree are
bound, the meaning cannot later change.
The return type of reify is Expr, which wraps
a typed tree with its TypeTag and some
methods like splice for transforming trees.
26. Inspecting the raw tree
Once you’ve reified an expression using the macro
universe, you can use showRaw to show the raw tree, which you
can use in a macro:
scala> showRaw(reify{ object MyOps { def add(a: Int, b: Int) = a
+ b } })
res16: String =
Expr(Block(List(ModuleDef(Modifiers(), newTermName("MyOps"), Tem
plate(List(Ident(newTypeName("AnyRef"))), emptyValDef, List(DefD
ef(Modifiers(), nme.CONSTRUCTOR, List(), List(List()), TypeTree(
), Block(List(Apply(Select(Super(This(tpnme.EMPTY), tpnme.EMPTY)
, nme.CONSTRUCTOR), List())), Literal(Constant(())))), DefDef(Mo
difiers(), newTermName("add"), List(), List(List(ValDef(Modifier
s(PARAM), newTermName("a"), Ident(scala.Int), EmptyTree), ValDef
(Modifiers(PARAM), newTermName("b"), Ident(scala.Int), EmptyTree
))), TypeTree(), Apply(Select(Ident(newTermName("a")), newTermNa
me("$plus")), List(Ident(newTermName("b"))))))))), Literal(Const
ant(()))))
27. Scala ToolBox: compile at runtime
Runtime classloader mirrors can create
a compilation toolbox whose symbol
table is populated by that mirror.
Want a tree? Use ToolBox#parse to
turn a string of code representing an
expression into an AST.
Have a tree? Use Toolbox#eval to spawn
the compiler, compiler in memory, and
launch the code.
See scala.tools.reflect.ToolBox for
more, as well as this StackOverflow
discussion.
28. Type erasure: fighting the good fight
$ scalac -Xshow-phases
phase name id description
---------- -- -----------
erasure 16 erase types, add interfaces for traits
When you inspect types at runtime, you will be missing some of
the type information that was available to the compiler during
stages before the JVM bytecode was generated.
If you want to mine types out of options, collections and
parameterized classes, you need to ask the compiler to stash the
type information where you'll be able to get to it at runtime.
29. Across the river
What ferries compiler type information to
runtime?
Before 2.10: Manifest[T]
After 2.10: TypeTag[T]
Request the compiler generate this information
using:
- using an implicit parameter of type Manifest or
TypeTag
- context bound of a type parameter on a
method or a class
- via the methods manifest[T] or typeTag[T]
30. Before Scala 2.10: manifests
The manifest is a shim where the compiler stores type
information, which is used to later provide runtime access
to the erased type as a Class instance.
scala> case class A[T : Manifest](t: T) { def m =
manifest[T] }
defined class A
scala> A("test").m
res26: Manifest[java.lang.String] = java.lang.String
scala> A(1).m
res27: Manifest[Int] = Int
31. Scala 2.10: type tag
Mirabile visu: instead of getting back a manifest, we get
back an actual type.
scala> case class A[T : TypeTag](t: T) { def tpe =
typeOf[T] }
defined class A
scala> A("test").tpe
res19: reflect.runtime.universe.Type = String
scala> A(1).tpe
res20: reflect.runtime.universe.Type = Int
32. Type arguments: before Scala 2.10
Using manifests:
scala> A(Map.empty[String, A[Int]]).m.erasure
res5: java.lang.Class[_] = interface
scala.collection.immutable.Map
scala> A(Map.empty[String, A[Int]]).m.typeArguments
res6: List[scala.reflect.Manifest[_]] =
List(java.lang.String, A[Int])
33. Type arguments: Scala 2.10
The parameterized types are now a list of types:
scala> A(Map.empty[String,A[Int]]).tpe.erasure
res17: reflect.runtime.universe.Type =
scala.collection.immutable.Map[_, Any]
scala> res10 match { case TypeRef(_, _, args)
=> args }
res18: List[reflect.runtime.universe.Type] =
List(String, A[Int])
34. Sadly…
The runtime reflection API is
not currently thread safe.
Keep an eye on this issue for
developments.
https://issues.scala-
lang.org/browse/SI-6240
Cheer up! The reflection used
in macros is not affected.
35. Reflection tools
The Scala REPL has a magnificent :power mode which is
not well explained. Examine its underpinnings here:
scala.tools.nsc.interpreter.Power
Get more details by using scalac to compile small test
files – start by playing around with the –Xprint:
compiler options:
scala.tools.nsc.settings.ScalaSettings
36. sbt project
To use Scala 2.10 reflection:
libraryDependencies <+= (scalaVersion)("org.scala-lang" %
"scala-compiler" % _)
To use pickled Scala signatures
libraryDependencies <+= scalaVersion("org.scala-lang" %
"scalap" % _)
37. Macros in the wild
• Spire – a numeric library for Scala (examples of
macros and
specializationhttp://github.com/non/spire
• Sherpa – a serialization toolkit and ‘reflection-less’
case class mapper for Scala
http://github.com/aloiscochard/sherpa
• sqlτyped – a macro which infers Scala types by
analysing SQL statements
https://github.com/jonifreeman/sqltyped
38. Things to read, things to watch
• Martin Odersky's Lang-NEXT 2012 keynote, Reflection
and compilers
• Paul Phillips ScalaDays 2012 presentation, Inside the
Sausage Factory: scalac internals
• Eugene Burmako’s Metaprogramming in Scala
• Daniel Sobral’s blog posts on JSON serialization with
reflection in Scala (Part I / Part II)
• StackOverflow posts tagged with Scala 2.10 reflection
• Scala issue tracker reflection tickets contain detailed
discussion and useful links
39. Thanks to…
• Eugene Burmako (@xeno_by) not only for many
helpful StackOverflow posts, but also his comments
on these slides
Follow me on Twitter for more interesting
presentations - @prasinous