Monday 24 June 2013

Adventures with Scala Macros - Part 2

The Adventures with Scala Macros series was published on the ScottLogic company blog. It is reproduced here.

Where next?

In part 1 we used a macro to generate some regular expressions and a case statement that used them:

What you’ve probably noticed though is that with this code, the regexes are parsed each time through the block, which isn’t going to be very good for performance. What we really want is to be able to put the regular expressions in an object so that they are parsed only once. But we’re using Scala 2.10, and all we have available are def (function) macros - we can’t generate any other types (although type macros do exist in the macro paradise).
Instead we need to create a value which then gets populated by a function that returns an object, like this:

So now we’ve got two functions for our generated code - and we know how to generate function bodies. However, we have another problem - each function will be generated by a separate macro, and we’ll need to know what the variables created in one macro are called when using them in the other macro. Fortunately we can just avoid that problem - each regular expression is generated from a class name, and each class name (with its package qualifier) is unique, so we can use the class name to generate the variable names (replacing . with $).

Code organisation

Our macro code is getting bigger, and I don’t want it to end up as a pair of unmanageble functions that go on for page after page - so I’m going to split the code up into two classes - the one that has the macro definitons in it and builds the resultant AST for the generated code, and another class that contains all the logic for analysing the compilation environment.
As documented on the def macros page on the Scala site, you can create other classes that use your macro context easily enough - you just have to give the compiler a little helping hand to work out where the context comes from. Of the two examples for doing this on that page, the second is (to me) much more readable, so we’ve got a new class:

And obviously we can initialise this just like any other class from within our macro implementations using val helper = new Helper(c). If, for tidyness, you want to import the functions defined in Helper, you can then do import helper.{c => cc, _}, which renames the c value from Helper so that it doesn’t collide with the c parameter from our function signature.
Moving the compilation unit processing code into the Helper, and adding some name processing so that the class name and package name are available to our macro, we end up with:

Object Creation

When you define an object in normal Scala, you just declare it, object myObject, because the syntactic sugar allows you to leave out a default constructor, and that it extends scala.AnyRef. In a macro you don’t have that luxury, so to define a new object, you do the following:

We already know how to put a block of code together from part 1, so all we need to do is merge the two together, and we get:

This can then be used by declaring val regexesUsingMacro = restapi.Generator.macroPaths in our unit test.
But wait - there’s a problem, isn’t there? That function is returning an object of type Any, so all our other generated code will know about it is that it’s an any - it won’t know anything about the vals we’ve added to it. Well actually, it turns out this isn’t a problem, but is intended that the return type should know more than its declaration specifies, if possible, as Eugene Burmako explains here.

Putting it all together

Now that we’ve got a macro that can be used as a val definition, we need to find that val and use it in our match expression. To find the val is simple enough - we just look for a ValDef that uses a Select for our function as the right hand side. However, if the user hasn’t defined such a val, we can’t continue - we need to tell the developer what they’ve done wrong. The macro Context includes functions to provide compiler warnings and errors, so we need to emit an error advising the developer how to fix it. The structure we end up with is as follows:

When integrated with the code we had for the match block from part 1, we end up with:

So now we’ve got our pattern matching working well, in the next article we can start calling an API to produce our endpoints.

No comments:

Post a Comment