• No results found

Metaprogramming can be described as the process used for building (meta-)programs, that are aware of the structure and behavior of other programs either by producing or by manipulating them [CI84]. In spite of being common practice to write meta-programs that are also the target program nothing forces them to be written in the same language. The approach can be used to generate programs that consist of well-known repetitive patterns, to enable frameworks with easy to use interfaces, or in a more general sense, to empower program’s capabilities.

2.3.1 Reflection

Reflection is the ability of a program to manage structures that keep track of its state and behavior during execution. The ability to acknowledge and reason about the program’s state is called intro-spectionwhile the ability to change the state or its interpretation is called intercession [BGW93].

1Meta-Object Facility [Obj] is the meta-model used by UML

This property enables the development of programs with higher complexity, such as, the genera-tion of programs by using reflecgenera-tion in a internal DSL (secgenera-tions2.2and2.3).

2.3.2 Macros

A macro is a feature related to programming languages that produces code based on a high-level pattern language [CR91]. It is used to extend languages’ capabilities in general, being often asso-ciated with the reduction of boilerplate code as it started to be used in low-level languages, such as Assembly, C, and Common Lisp. Macros can be expanded at compile-time or before compilation, normally using preprocessors as in the C language (Source2.1).

1 #define PI 3.14159

2 float circle_perimeter(float radius)

3 {

4 return 2 * PI * radius; // transformed to 2 * 3.14159 * radius

5 }

Source 2.1: Example of a simple macro in C

2.3.3 Metaprogramming with Scala

Metaprogramming for Scala is still experimental and is currently provided by the scala.reflect API and the macro paradise compiler plugin [Bur14]. The scala.reflect API is available since version 2.10 of Scala and was built to support full run-time reflection, as the the Java reflection API only exposed Java elements, and to support compile-time reflection in the form of macros [MBH]. The macro paradise plugin adds functionalities to the scala.reflect API and is distributed separately from the official Scala distribution, as it is used for experimentation of new features [Bur14]. The following sections2.3.3.1and2.3.3.2present how reflection and macros can be explored in Scala.

2.3.3.1 Reflection

The scala.reflect API can be used for run-time or compile-time reflection. Run-time reflection allows inspection of a object’s type, including generic types, instantiation of new objects, and access or invocation of that object’s members. On the other hand, compile-time reflection is used for manipulation of a program’s abstract syntax tree (AST), types, and symbols.

In other to achieve run-time reflection of a given compile-time type T the compiler encap-sulates all of its information in a TypeTag[T] type. This happens whenever is used an implicit parameter or a context bound that will translate in a implicit TypeTag[T] like in Source2.2. The API defines the concept of symbol as the binding between names and entities, such as classes or methods.

1 import scala.reflect.runtime.{universe => ru}

2 def getType[T: ru.TypeTag](obj: T) = ru.typeOf[T]

3 // getType: [T](obj: T)(implicit evidence$1: ru.TypeTag[T]) ru.Type 4

10 // Iterable[ru.Symbol] = List (constructor List, method companion, method isEmpty)

Source 2.2: Access to a object’s type and its declarations

Access to packages, objects, classes, fields, methods, variables, and instances are achieved using mirrors. For example, the continuation of Source2.2in Source 2.3 describes the steps to call the method isEmpty on the original list using a classloader mirror, Mirror, and two invoker mirrors, InstanceMirror and MethodMirror.

1 val rMirror = ru.runtimeMirror(getClass.getClassLoader) 2 // ru.Mirror = JavaMirror ...

3

4 val iMirror = rMirror.reflect(list)

5 // ru.InstanceMirror = instance mirror for List(1, 2, 3) 6

7 val mMirror = iMirror.reflectMethod(decls.last.asMethod)

8 // ru.MethodMirror = method mirror for def isEmpty: Boolean (bound to List(1, ...

9

10 mMirror() 11 // Any = false

Source 2.3: Method invocation through reflection

2.3.3.2 Macros

“Macros are functions that are called by the compiler during compilation” [Bur]. These functions can generate, analyze and typecheck code. They also have the capability to abort compilation or report warnings.

The most basic flavor of Scala macros are the def macros that are methods expanded at compile time, transforming their declaration into code with similar interface. These macros can be defined

“either inside or outside of class, can be monomorphic or polymorphic, and can participate in type inference and implicit search” [Bur13]. As these macro calls have nothing different from normal method calls many features can be empowered without users even noticing of their use.

1 def printf(format: String, params: Any*): Unit = macro impl

2 def impl(c: Context)(format: c.Expr[String], params: c.Expr[Any]*):

c.Expr[Unit] = {

3 ...

4 c.Expr[Unit](q"print($result)")

5 }

6

7 printf("Hello %s!", "John") // Hello John!

Source 2.4: Example of a def macro

The implementation parameters and result are of type c.Expr[T] instead of any original type T, as in Source2.4. It is possible to access to a context in which the macro is being used, enabling more flexibility. The use of quasiquotes2increase readability, just like in the line four of the ex-ample. Other types besides Expr may be used for building an AST such as ClassDef, ModuleDef, ValDef, DefDef, TypeDef, Literal, Constant, and more. This allows parameterized implementation of type providers as shown in Source2.5.

1 def h2db(connString: String): Any = macro ...

2 val db = h2db("jdbc:h2:coffees.h2.db") 3

4 // expands into 5 // val db = { 6 // trait Db {

7 // case class Coffee(name: String, price: Decimal) 8 // val Coffees: Table[Coffee] = ...

9 // }

Source 2.5: Example of a type provider def macro [BOV+13]

Another macro flavor is implicit macros that allows methods to be called without having the users write the calls explicitly, through, for example, materialization of type class instances and implicit conversions. These parameters are inferred from the current scope based on the type of the target, that must be declared with the keyword implicit [Bur13]. When the use of this kind of macros leads to boilerplate code, the combination with def macros can solve the problem as shown in Source2.6.

2String interpolators that build code

1 trait Showable[T] { def show(x: T): String }

2 def show[T](x: T)(implicit s: Showable[T]) = s.show(x) 3

4 implicit def materialize[T]: Showable[T] = macro ...

5

6 show(person)

7 // show(person)(materialize[Person]) --- implicit macro expansion 8 // show(person)(new Showable[Person] { ... }) --- def macro expansion

Source 2.6: Materialization with implicit macros [BOV+13]

The last major macro flavor is macro annotations that can be used to transform definitions such as classes, objects and fields. They allow not only the expansion of classes but also to create or modify companion objects as applied in Source2.7. However expansions are constrained as expanded top-level classes and objects must have the same name as the annottee. Annotated expressions are only typechecked after expansion, enabling greater flexibility.

1 @case class C(x: Int) 2 // expands into 3 // class C(x: Int) {

4 // /* standard case class methods like toString */

5 // }

6 // object C {

7 // /* standard case companion methods like unapply */

8 // }

Source 2.7: Example of a annotation macro [Bur13]

Related documents