Scala

Starting with Scala Macros: a short tutorial

Using some time during the weekend, I decided to finally explore one the new features in the coming Scala 2.10, macros.

Macros are also written in Scala so in essence a macro is a piece of Scala code, executed at compile-time, which manipulates and modifies the AST of a Scala program.

To do something useful, I wanted to implement a simple macro for debugging; I suppose I’m not alone in using println-debugging, that is debugging by inserting statements like:
 
 

println('After register; user = ' + user + ', userCount = ' + userCount)

running a test, and checking what the output is. Writing the variable name before the variable is tedious, so I wanted to write a macro which would do that for me; that is:

debug('After register', user, userCount)

should have the same effect as the first snippet (it should generate code similar to the one above).

Let’s see step-by-step how to implement such a macro. There’s a good getting started guide on the scala macros page, which I used. All code explained below is available on GitHub, in the scala-macro-debug project.

1. Project setup

To experiment comfortably we’ll need to setup a simple project first. We will need at least two subprojects: one for macros, and one for testing the macros. That is because the macros must be compiled separately and before and code that uses them (as they influence the compilation process).

Moreover, the macro subproject needs to have a dependency on scala-compiler, to be able to access the reflection and AST classes.

A simple SBT build file could look like this: Build.scala.

2. Hello World!

“Hello World!” is always a great starting point. So my first step was to write a macro, which would expand hello() to println('Hello World!') at compile-time.

In the macros subproject, we have to create a new object, which defines hello() and the macro:

package com.softwaremill.debug

import language.experimental.macros

import reflect.macros.Context

object DebugMacros {
  def hello(): Unit = macro hello_impl

  def hello_impl(c: Context)(): c.Expr[Unit] = {
    // TODO
  }
}

There are a couple of important things here:

  1. we have to import language.experimental.macros, to enable the macros feature in the given source file. Otherwise we’ll get compilation errors reminding us about the import.
  2. the definition of hello() uses the macro keyword, followed by a method which implements the macro
  3. the macro implementation has two parameter lists: the first is the context (you can think about it as a compilation context), the second mirrors the parameter list of our method – here it’s empty. Finally, the return type must also match – however in the method we have a return type unit, in the macro we return an expression (which wraps a piece of an AST) of type unit.

Now to the implementation, which is pretty short:

def hello_impl(c: Context)(): c.Expr[Unit] = {
  import c.universe._
  reify { println('Hello World!') }
}

Going line by line:

  1. first we import the “universe”, which gives convenient access to AST classes. Note that the return type is c.Expr – so it’s a path-dependent type, taken from the context. You’ll see that import in every macro.
  2. as we want to generate code which prints “Hello World!”, we need to create an AST for it. Instead of constructing it manually (which is possible, but doesn’t look too nice), Scala provides a reify method (reify is also a macro – a macro used when compiling macros), which turns the given code into an Expr[T] (expressions wrap an AST and its type). As println has type unit, the reified expression has type Expr[Unit], and we can just return it.

Usage is pretty simple. In the testing subproject, write the following:

object DebugExample extends App {
  import DebugMacros._
  hello()
}

and run the code (e.g. with the run command in SBT shell).

3. Printing out a parameter

Printing Hello World is nice, but it’s even nicer to print a parameter. The second macro will do just that: it will transform printparam(anything) into println(anything). Not very useful, and pretty similar to what we’ve seen, with two crucial differences:

def printparam(param: Any): Unit = macro printparam_impl

def printparam_impl(c: Context)(param: c.Expr[Any]): c.Expr[Unit] = {
  import c.universe._
  reify { println(param.splice) }
}

The first difference is that the method accepts a parameter param: Any. In the macro implementation, we have to mirror that – but same as with the return type, instead of Any, we accept an Expr[Any], as during compile-time we operate on ASTs.

The second difference is the usage of splice. It is a special method of Expr, which can only be used inside a reify call, and does kind of the opposite of reify: it embeds the given expression into the code that is being reified. Here, we have param which is an Expr (that is, tree + type), and we want to put that tree as a child of println; we want the value that is represented by param to be passed to println, not the AST. splice called on an Expr[T] returns a T, so the reified code type-checks.

4. Single-variable debug

Let’s now get to our debug method. First maybe let’s implement a single-variable debug, that is debug(x) should be transformed into something like println('x = ' + x).

Here’s the macro:

def debug(param: Any): Unit = macro debug_impl

def debug_impl(c: Context)(param: c.Expr[Any]): c.Expr[Unit] = {
  import c.universe._
  val paramRep = show(param.tree)
  val paramRepTree = Literal(Constant(paramRep))
  val paramRepExpr = c.Expr[String](paramRepTree)
  reify { println(paramRepExpr.splice + ' = ' + param.splice) }
}

The new thing is of course generating the prefix. To do that, we first turn the parameter’s tree into a String. The built-in method show does exactly that. A little note here; as we are turning an AST into a String, the output may look a bit different than in the original code. For vals declared inside a method, it will return simply the val name. For class fields, you’ll see something like DebugExample.this.myField. For expressions, e.g. left + right, you’ll see left.+(right). Not perfect, but readable enough I think.

Secondly, we need to create a tree (by hand this time) representing a constant String. Here you just have to know what to construct, e.g. by inspecting trees created by reification (or reading Scala compiler’s source code).

Finally, we turn that simple tree into an expression of type String, and splice it inside the println. Running for example such code:

object DebugExample extends App {
  import DebugMacros._

  val y = 10

  def test() {
    val p = 11
    debug1(p)
    debug1(p + y)
  }

  test()
}

outputs:

p = 11
p.+(DebugExample.this.y) = 21

5. Final product

Implementing the full debug macro, as described above, introduces only one new concept. The full source is a bit long, so you can view it on GitHub.

In the macro implementation we first generate a tree (AST) for each parameter – which represents either printing a constant, or an expression. Then we interleave the trees with separators (', ') for easier reading.

Finally, we have to turn the list of trees into an expression. To do that, we create a Block. A block takes a list of statements that should be executed, and an expression which is a result of the whole block. In our case the result is of course ().

And now we can happily debug! For example, writing:

debug('After register', user, userCount)

will print, when executed:

AfterRegister, user = User(x, y), userCount = 1029

Summing up

That’s quite a long post, glad somebody made it that far. Anyway, macros look really interesting, and it’s pretty simple to start writing macros on your own. You can find a simple SBT project plus the code discussed here on GitHub (scala-macro-debug project). And I suppose soon we’ll see an outcrop of macro-leveraging projects. Already there are some, for example Expecty or Macrocosm.
 

Reference: Starting with Scala Macros: a short tutorial from our JCG partner Adam Warski at the Blog of Adam Warski blog.

Adam Warski

Adam is one of the co-founders of SoftwareMill, a company specialising in delivering customised software solutions. He is also involved in open-source projects, as a founder, lead developer or contributor to: Hibernate Envers, a Hibernate core module, which provides entity versioning/auditing capabilities; ElasticMQ, an SQS-compatible messaging server written in Scala; Veripacks, a tool to specify and verify inter-package dependencies, and others.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button