Wednesday 27 March 2013

Adventures with Scala Macros - Part 1

With the arrival of Scala 2.10 came two interesting new features: the reflection API and macros. The possibilities for macros are many, and one that I want to explore is how you can use a macro to create a Play Framework REST API from a data model represented as a set of case classes. In this and subsequent articles I'll be using a simple book store type application with case classes for Book, Author and Category in the model package.

Normally in the Play Framework you'd declare your rest API in the routes file, something like:

However, as you can't use custom Scala in routes (since Play 2.0, anyway), we'll be designing a macro for use in the onRouteRequest function of the Global object that extends the GlobalSettings trait. This function takes a request and returns an Action. The selection of what Action to return will be based on the URL, so we'll have to match the path to the right case class.

In this first article, we'll be working out how to do that matching based on an unknown number of case classes in the application's model.

If we weren't using a macro, we'd probably do something like:

Our macro will need to generate the same function by iterating over all the found case classes. Firstly we need to declare the macro, and then complete the implementation, as follows:

What we've done here is declare a macro as described on the documentation linked above. The macro context's universe is imported - it is important to make sure you import the universe exactly as above, as if you use a import like c.{universe => u} then for some reason erasure stops them working properly. We've then used the universe's reify function to return an expression for Some("TODO") - the reify function basically turns the enclosed scala into an AST tree expression.

Finally, we need a unit test to check we've implemented the method correctly:

The caseUsingMacro function will have its body replaced with the result of macro call - i.e. it is here that the function is going to be generated, replacing Macro.macroCase(s).

Now we're ready to start implementing the macroCaseImpl function from above - but what do we need to return, and how can we debug our macro's output to see how we're doing. Fortunately, there are two very useful functions in the universe - show and showRaw. Given an AST tree, the show function will return a pretty-formatted string of the equivalent scala code. The showRaw function will return a string of the AST making up the tree. As what we're trying to implement is something that for our 3 case classes will look like the caseStmt function above, we can get a good hint for what we need to end up with to look at its equivalent AST, which we can get by doing:

The results of this println will be output to the console for the compilation [1]. We get something like this:

We'll break this apart into useful fragments as we get to using them.

So, first we need to get a list of all the case classes in the model package. To do this, we can iterate over the context's enclosingRun.units property, matching on elements of the AST tree for each compilation unit to find out the package name, and then checking that we've got a case class. The rough code we need for this is:

We then need to implement the packageFinder and classFinder functions. To check that the compilation unit is in the desired package, the first of these needs to return true when the Tree instance is a PackageDef with a name equal to, or starting with, the second argument, so:

Then to check that the compilation unit contains a case class, the second function needs to return true if the Tree instance is a ClassDef with a CASE modifier:

So now we've got a list (well, in fact it's an Iterator) of the case class compilation units in the package we need to turn them into regular expressions to match against the URL, and into case statements to do the matching.

Firstly, the regular expressions. If we take the AST that we generated above, we can see that the value definition for one regex is:

Of the four parameters to the ValDef creator, the first, Modifiers(), and third, TypeTree(), we can take straight into our generated AST. The second parameter we can generate using a context function to create a new unique val name, c.fresh("regex$"). The last parameter is using the implicit augmentString function on a String so that we can then call the r function to turn it into a Regex. However, we can do this more simply using the reify function combined with a string expression for the regex we want to use, which we can construct in the macro using the class name:

Here we've created an Expr object of a constant that is the regex string, and then used the splice function, which is a special function that allows an Expr object to be grafted into the middle of the block being reified. We then just call the r function inside the reify block for it to be turned into a Regex.

The other bit of AST that we need to generate for each class name is the case statement of the match block. Again, from the above, we can see that this should look something like:

Reformatting, this becomes a little easier to understand:

Notice that again we've swapped the newTermName("bookRegex") for valName which is the fresh name we got using the "regex$" base name, the name of the defined value that is the Regex.

Now, we need to collect up all the val definitions and case statements so that they can be assembled into a block of scala code. We also need to add the default case:

Putting all this together, and getting the class name from the AST tree for the compilation unit, we get:

So, we assemble the cases and regexes into a block. The return of the block is going to be the result of the match statement, and the regexes are declared before that final statement, so (remember, s is the original parameter to the macroCaseImpl function):

To check that this AST tree really is the Scala code we hope it is, we can use the show function I mentioned before:

And finally, we return the Expr that contains our block of scala:

And when we compile, we see that our Block tree is equivalent to:

Here we can see the generated val names and the familiar regexes and match-case block returning the Option values that are expected. On running the unit test, we find it now passes.

Phew!

In the next stage of this adventure, I'll be extending this to use persistence of case classes as declared in their companion object, and deducing what REST operations are available based on what data access functions are exposed.





[1] If you're using the Scala IDE it might take you a while to find the compilation output - what you need to do is turn up the logging to DEBUG in Window -> Preferences, Scala -> Logging. Your console output will then appear in [workspace]/.metadata/.plugins/org.scala-ide.sdt.core/scala-ide.log

2 comments:

  1. Hey! That's an interesting idea! What would you do to make it work with separate and/or incremental compilation?

    ReplyDelete
    Replies
    1. Yep, that's something I'm going to have to cover too. It should be possible to also get model classes from c.classPath by processing the jar files and directories that the URLs point to.

      Delete