Using Macro Annotations to Reduce Boilerplate

by / / Engineering


Often when modelling certain types of problems Scala (e.g., using third-party data), I’ll find myself needing a bunch of similar but slightly different case classes under a common trait. While this isn’t a big deal with one or two classes, when you have a large number it becomes tedious to do things like change, add, or remove fields that all classes have in common:

You can of course add the common fields to the trait, but these are no longer passed as constructor parameters so constructing instances becomes ugly:

Extending an abstract class makes construction nice again, but now you have to type everything an additional time:

Wouldn’t it be nice to extend a trait and have undeclared vals become contructor parameters without have to explicitly write them out in every implementation? With a little help from macro annotations you can do just that:

How does that work? The short version is that the Extends macro annotation finds all undeclared vals in the trait Base, injects them as constructor params for A and B, and adds Base to the list of traits they extend at compile time. But let’s dive into the code.

As an aside, I’d like to give a hearty thanks to this post from Martin Raison at Kifi andthis one from Imran Rashid for helping me figure all this out.


To start off, we have to import a few things:

Next, we define the annotation class, which requires the macro paradise compiler plugin. (If you haven’t used it before, just addaddCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) to your build.sbt.) Note that this class doesn’t really do anything; the actually implementation is defined in its companion object.

Now let’s get to the meat of the implementation. As you can see above, we’ll be defining a method called impl in the Extends object that acts as a macro, meaning that it receives the context (a whitebox one in this case) and returns a modified expression. There are a handful of other methods referenced by or defined within our impl so the flow of the code might get lost here, but you can find the full implementation in this gist.

To start off, we import the universe from our context and then start inspecting the trait we’re mixing in. First, we extract the trait we’re extending by looking at the annotated portion of the tree and using a quasiquote to unlift its components, and then create a “companion” Type instance.

We next get a list of all undefined vals in our target trait. Note that this is exactly how you’d do this from a TypeTag with reflection at runtime, but since this is a macro it’s run at compile time.

Next up, a few helper methods. First of all, one to deconstruct our target case class, again using quasiquotes:

Then one to add our trait’s values to the existing constructor arguments in the target case class. I find it slightly nicer to have the common fields at the beginning of the argument list.

And finally a method to return the modified case class with its new parameter list and our trait mixed in.

Now just have to apply this method to the annotated classes and return them.

And that’s the entire implementation. Now, it’s worth noting a few drawbacks and limitations. First, even if you package up this macro as a standalone entity, projects using it directly must include the macro paradise plugin (though projects using those projects do not). Next, as you can see in the code above this only works on case classes. I don’t think there’s any reason it couldn’t work on regular classes with a few changes, but I haven’t needed it. You can only extend one trait using this technique, though you could easily work around this by creating a trait that mixes together all the traits you want to extend. I haven’t tried using this in Eclipse, but IntelliJ shows a lot of spurious errors when using classes defined with this annotation (though only within the project in which they’re defined). Finally, this was developed for a specific use case and has only been tested under those conditions, so I can’t guarantee that this won’t break down in other situations.

All that said, I’ve found this a handy solution for eliminating boilerplate without losing type safety. Once again, the the full implementation is in this gist; feel free to leave any comments or suggestions there.