I. Introduction. What are macro annotations?
With macro annotations you can annotate any definition with something that Scala recognizes as a macro and it will give you ability to modify arbitrarily this definition. Personally speaking, it is my favorite type of macro with many cool use cases.
Code is on my github, branch part-2. Let’s start!
II. Setup
Setup is the same as in part 1
III. Example no.2: Benchmark
In part 1 we created pretty much useless macro, just to become familiar with new quasiquotes api and macro project structure. Now let’s try to create something more useful. Imagine following problem. We want to benchmark methods. We would like to check how much time takes method to execute. This is our method. As simple as it could be. Type parameter is a fake, just to make this method look more fancy.
1 2 3 4 5 6 | |
Now how to measure running time of body of this method? This is one possibility:
1 2 3 4 5 6 7 8 9 10 11 12 | |
So we wrap body of function with time snapshots then we assign result of #testMethod() to val result, then we println time difference and return result. The problem with it is we had to touch #testMethod() and modify it. Better solution would be to not touch code of #testMethod() at all. Moreover it is boilerplate code, not interesting for developer at all.
In Test.scala you will find one possible solution could be, for example:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
And we want that result of above code to be the same as previously. We can easily do this with macro annotations.
Look at Main.scala, there’s usage of our methods:
1 2 3 4 | |
Now check the implementation of @Benchmark
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | |
There are few differences between def macros and macro annotations when looking at their implementations.
First of all class Benchmark has to extend StaticAnnotation trait.
Second difference is we need to implement macroTransform method which take annottees: c.Expr[Any]* as argument. You might think about Expr as wrapper around AST.
Let’s focus on implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
What is going on here?
Annotees is Seq of annotated definitions. We want to have them in form of AST this is why we map over this Seq and return new Seq with AST nodes.
Next outstanding feature of quasiquotes is extractor pattern and this is why we can use pattern matching over Tree node. And you will find in docs syntax summary which pattern corresponds to which scala construction. In our case we will annotate methods so we used syntax for extracting method tokens.
Look at this weird syntax:
1
| |
Ok, so we are extracting methodName but what are those “..” and “…” signs means?
Let’s start with ..$- this pattern expects List[universe.Tree]. And this is nice because our annotated method could take many type parameters.
And what is …$- this pattern expects List[List[universe.Tree]]. This is becase our method can take many parameters sets, so it could look like this one:
1 2 3 | |
In conclusion we are matching each annottee to this method pattern, we extract from annottee(method in our case) all potentially useful elements like method name or parameter list etc. And we are returning new collection of modified AST’s. In this example we return the same method signature with modified body. You see that inside quasiquote we are doing what we actually expected. So we wrap code with those timestamps, and println time difference between endTime and startTime.
If we would apply @Benchmark to some other definition like class or val or whatever, then preventing from match error, I’ve added default case:
1
| |
IV. Example no.3: Talking Animal
Macros can be treated as some kind of magic. Previous examples have shown that they can modify AST and slightly change behaviour of your code. But they can do much more than that. Let’s look at Main.scala:
1 2 3 4 5 | |
Something is wrong here, I personally use Intellij Idea and it doesn’t see this method sayHello on object Dog. But this code compiles properly. To solve this mystery open Animal.scala:
1 2 3 4 5 6 | |
We have got simple case class Dog which extends Animal trait. It doesn’t have method sayHello implemented, but there is @TalkingAnimalSpell annotation. So let’s look at implementation of it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | |
Difference between this and previous benchmark example is in pattern matching. Case condition is different. In this TalkingAnimalSpell macro we want to annotate classes not methods like in benchmark. In quasisquotes syntax summary you will find pattern for classes. And because I want this annotation to work only with classes which extends Animal trait I specified it explicitly in pattern case.
What this macro does is returning the same object but with added new method sayHello which println the name argument. Writting this blog post I’ve noticed a bug. Does it return exactly the same object? Modify a little bit Dog class:
1 2 3 4 5 6 | |
And invoke it in Main.scala
1 2 3 4 5 | |
And you get compilation error :) Everything looks fine, method is implemented Idea can see it but you’ve got compilation error. What is wrong? Clearly @TalkingAnimalSpell is really some kind of magic, look at its implementation again:
We are returning class with the same signature, but we are missing existing body of annotated class. Let’s add missing body:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
And run code again, everything should compile fine. So as you see you can make arbitrary change with you annotated code. With great power comes great responsibility. I encourage you to play a little bit with this example.
V. Homework
Maybe some kind of good homework exercises would be
1) Imagine that you are funny software developer who likes to make jokes. Change implementation and returning type of
1
| |
2) change implementation of sayHello to println each animal attributes. For example, you’ve got this class with @TalkingAnimalSpell annotation:
1 2 | |
And invoking sayHello on this object:
1 2 3 4 | |
should result with this println:
1
| |
3) Change @Benchmark annotation. It should be possible to annotate scala object and it will add this benchmark code to each non private method inside this object. So this code, after compilation:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
should work the same as this code:
1 2 3 4 5 6 7 8 9 10 11 12 | |
VI. Summary of part II
In this post, we explored another type of macro: macro annotations. But there are still some question marks. One of them is highlighted by last example with @TalkingAnimalSpell and exercise no. 1 from your homework. Intellij Idea coding assistance is based on static code analysis and it is not aware of AST changes, so there is lack of support for macros.
You probably feel disappointed now. What if your macro generates methods that are invisible for your IDE? Your code will be highlighted with red , you wouldn’t know returning type of generated methods, moreover somebody will use macros to change existing code etc. Without documentation this macro could be more confusing than useful. Thankfully Intellij Idea has API for writing plugins to support macros. In Part 3 of this blog series, we will create plugin for Intellij Idea to add support to our @TalkingAnimalSpell macro annotation. See you in Part 3!