Scala Macros

  Eugene Burmako.        2012-02-01 00:12:15       7,000        0    

This is the home page of project Kepler, an ongoing effort towards bringing compile-time metaprogramming to Scala. Our flavor of macros is reminiscent of Lisp macros, adapted to incorporate type safety and rich syntax. Unlike infamous C/C++ preprocessor macros, Scala macros: 1) are written in full-fledged Scala, 2) work with expression trees, not with raw strings, 3) cannot change syntax of Scala. You can learn more about our vision of metaprogramming from our talks.

We propose to enrich Scala programming language with three flavors of type-safe compile-time macros: macro defs, macro types and macro annotations. These kinds of macros can be used wherever their vanilla analogues are used, but they are treated differently by the compiler, providing novel ways to analyze and generate code.

Macro defs are about morphing a program during the compile-time. Whenever a compiler sees an invocation of a method declared as a macro def, it immediately calls its implementation that analyzes the program and/or generates new code at the call site (so called "macro expansion").

The following example describes the type-safe printf function. Being a macro, printf formats the output during the compile-time, which provides safety guarantees inaccessible to regular approaches. This notion can be generalized to serve the needs of many other domains, the topic that is dwelled upon in the "Integration of external DSLs" case study on this site.

However, let's proceed with the code example that outlines the implementation and usage of the printf macro along with the result of its expansion. If certain aspects of the code are unclear, consult our walkthroughs (English, Russian) or drop us a line at dev@scalamacros.org:

// printf gets run during the compile-time and operates on ASTs
// format, params and the return value are of type Tree
// type annotations say that ASTs must type check to corresponding types
macro def printf(format: String, params: Any*) {
  // macros can call arbitrary functions, e.g. stuff from Scala library
  // parse is a custom function of type (Tree, Tree) => (Tree*, Tree*)
  val (evals, refs) = parse(format, params)

  // evals and refs are regular Scala values and can be manipulated as usual
  // c"..." denotes a quasiquotation, i.e. stuff of type Tree
  // "$x" is a splice that inserts x of type Tree into another Tree
  val seq = evals + refs.map(x => c"print($x)")

  // macro returns an AST that is then inlined into the call site
  // this quasiquotation wraps all the code generated into a block and returns it
  c"$seq"
}

// macro defs are called as regular functions
// it's also possible to declare macro defs as class members
// and invoke them using infix syntax
printf("Value = %d", 123 + 877)

// the code below represents the expansion of the above one-liner
// note that "123 + 877" has been pasted into the expansion verbatim
// this is because it was passed to the macro as an AST, not as a value
val p1 = (123 + 877): Int
print("Value = ")
print(p1)

Macro types are used to parametrically generate classes and traits that can be utilized directly, extended or mixed in, similarly to vanilla Scala classes. Moreover, macro types can also declare new package objects full of various definitions that can be imported using regular Scala import syntax.

This facility can be leveraged to achieve something similar to type providers from F#. After creating a suitable macro type for such domains as database access, inter-process interoperability, web services, the programmer is relieved from the necessity to generate and foster boilerplate classes. Since macro types are based on a solid foundation of a macro system, they provide more flexibility than ad-hoc approaches to these problems.

The notion of type providers is elaborated in greater detail in the "Type providers" case study, but for now let's just proceed with the code. Same as before, if something is confusing, take a look at the walkthroughs (English, Russian) or ask us at dev@scalamacros.org:

// macro types are, in essence, the same as macro defs
// they are also functions that take ASTs and produce ASTs
// take a look at the body of this macro in the "Type Providers" use case
macro trait MySqlDb(connString: String) = ...

// analogously to macro defs that can be used in place of regular defs,
// macro types can be used wherever you can use a type
// in this example, we mix in a macro trait into a normal class
// under the covers the macro will produce a synthetic trait that will be used
// as usual, all this will happen during the compile-time
type MyDb = Base with MySqlDb("Server=127.0.0.1;Database=Foo;")

// macro types generate fields, methods, inner classes and whatever else
// this "virtual" code can be used as if it was written manually
import MyDb._
val products = new MyDb().products
products.filter(p => p.name.startsWith("foo")).toList

Macro annotations can be used to perform final postprocessing on program elements (classes, methods, expressions) after all other macros have been expanded. Macro annotations look in the very same way as regular annotations do, however, they are not just static participants of the compilation pipeline, but can actively participate in it.

Potential areas of applicability of this feature include aspect-oriented programming, implementing new idioms (e.g. with macro annotations it's possible to introduce @lazy parameters or even work on sub-method level to, say, bless certain variables) and automatizing chores on a small scale (read up more on that in the "Generation of boilerplate" case study).

The example below features the @Serializable macro that reflects upon members of the annotated class, generates serialization/deserialization routines and implements the Serializable interface that exposes them (compiler API we're using here is completely fictional, it will be refined when Scala reflection API gets stabilized):

// in line with other flavors of macros, macro annotation transform ASTs
// however, the transformee does not come from the arguments list
// (after all, annotations might have arguments of its own)
// instead, the affect program entity is provided in a special implicit parameter
macro annotation Serializable(implicit ctx: AnnotationContext) = ...

// annotations get applied after everything else (other macros, syntactic sugar)
// that's why Serializable will see all the members of Person
// regardless of whether they come from a case class sugar
// or from Entity (be it a regular class or a macro class)
@Serializable
case class Person(firstName: String, lastName: String) extends Entity { ... }

Macros significantly simplify code analysis and code generation, which makes them a tool of choice for a multitude of real-world use cases. Scenarios that traditionally involve writing and maintaining boilerplate can be addressed with macros in concise and maintainable way. Therefore we believe that macros are a valuable asset to Scala programming language.

Source:http://scalamacros.org/index.html

SCALA  EFFICIENCY  MACRO  MAINTAINEBILITY 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

How programmers see some customers