Scala Unless-when via Macros
Last week, I decided to take a stab at learning Scala macros. I had played around with macros when I wrote Scheme for a living (yes, believe it or not, these places exist…and existed long before Clojure made Lisp hip again), but the complexity of Scala’s macros always put me off (if you don’t believe me, check out the example given in the offical docs for a simple print macro).
In Scala, things are not so simple, but with the introduction of quasiquotes and some refinements brought by Scala 2.11, things are smoother. Still, for a guy like me, the documentation was both sparse and DRY. Since I learn best when I’m actively engaged in building something, I decided to try writing the run-of-the-mill unless-when macros in Scala.
This post aims to summarise my journey towards implementing unless-when and hopefully along the way make Scala macros accessible, at least at an introductory level, for Most People. There are already a few Scala macro blog posts out there but another one can’t hurt.
Note: this blog post aims to explore macros as they are usable in Scala 2.10+. It also focuses on implementing macros with quasiquotes, as using them is more human-friendly than manually constructing Abstract Syntax Trees (AST).
Goal
For those unfamiliar with when
and unless
: the basic idea is that when
is an if
without an else, and unless
is it’s opposite. The main reason for their existence is to make code more readable by adding a tiny bit of syntatic sugar. Without further ado, an example of what we want to achieve
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Since we’re writing Scala, it would be nice if these constructs returned something useful; using the Option monad seems reasonable: If the block is run, we return the result in a Some and otherwise return a None. This tutorial is a good guide for Options in case you are unfamiliar with the concept.
Macro-y basics
Taking a look at the documentation, you will quickly notice the general pattern for implementing a simple Scala macro
1 2 3 4 5 6 7 8 9 10 |
|
What does this mean? Let’s break it down:
import scala.language.experimental.macros
andimport scala.reflect.macros._
are standard Scala imports that allow us to play around with macros. What’s not listed in this example is the declaration that your project depends onscala-reflect
. You can do so by adding the following to your build.sbt:libraryDependencies ++= Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value)
def meth[A](x: A): A
this is still just normal Scala code that we would normally see. It simply declares a method belonging to the Example singleton that is parameterised on the input type, and we want to make sure that the output type matches this type (e.g. if we invokemeth
with anInt
, we expect the output to be anInt
because that is the contract of the method). For more info on writing parametric polymorphism, please check out this guide:macro implRef[A]
this is where things start looking macro-ish. Themacro
keyword lets the compiler know that the body of this method is going to be implemented via a macro definition, in this caseimplRef
.def implRef[A: c.WeakTypeTag](c: Context)(x: c.Expr[A]): c.Expr[A]
.. wow. This itself needs to be broken down:def implRef[A: c.WeakTypeTag]
The first partdef implRef
is still standard Scala(c: Context)
(we’ll cover[A: c.WeakTypeTag]
in a bit). In this part,(c: Context)
declares that the first argument passed to the macro implementation must be a Context. This is a requirement for playing around with Scala macros, and is actually passed by the compiler when it invokes macro expansion, so that you can write code that accesses the compiler API.[A: c.WeakTypeTag]
This is a bit mischievous because we combine Scala-shorthand for typeclasses with macro-magic. This probably deserves a post in and of itself, but for now, please consider this to mean “A is a type parameter passed during macro invocation, but we must ALSO have in scope a WeakTypeTag coming from the Context that is parameterised to type A, which can be written in full as c.WeakTypeTag[A]”. This WeakTypeTag business is required so that we can pass along the type parameter frommeth
into theimplRef
macro expansion implementation, allowing us to have a type parameterised macro definition.- For more information on type classes and the shorthand we use here, I highly recommend this blog post on type classes
(x: c.Expr[A])
means that the first non-Context parameter of the macro implementation (remember that the first one is always taken by the compiler and must be a Context) isx
and it is ac.Expr[A]
. It is important that the name of the parameter matches that used in the invoking method (see howmeth
also hasx
as the first parameter).c.Expr
is type of object that wraps the abstract syntax tree that represents the input to the invoking function, and it is typed to A.- NOTE: since the argument’s type is
c.Expr
(essentially an abstract syntax tree), any expression passed to the methodmeth
actually may not get invoked or evaluated even though it is not a pass-by-name parameter. In other words, while the macro is expanding, it acts like a pass-by name parameter and is “lazy”.
- NOTE: since the argument’s type is
: c.Expr[A]
all this means is that the result of the macro expansion is also ac.Expr
type parameterised to A.
Quasiquotes
Quasiquotes are not a Scala-exclusive construct, and a Google search will show that they are used in other languages that support metaprogramming, like Scheme.
In short, they offer the macro programmer an easy way to manipulate or create abstract syntax trees without having to build them manually. This makes them extremely helpful in Scala because:
1. Scala syntax does not map to ASTs easily like Lisps
2. Scala is typed, which means your manually-built AST also needs typing…which wraps non-macro-land types (notice how a normal type parameter like [A]
becomes c.Expr[A]
… that’s twice as many characters !)
Quasiquotes allow us to use string-interpolation-like syntax to interpolate elements into a tree as we define it.
For example:
1 2 3 4 5 |
|
The above example was taken from the official documentation on quasiquotes, which I highly recommend you take a look at if you find the rest of this post hard to follow.
Implementation
For when
, we know that we roughly want the following:
1
|
|
To expand via our macro into the following (yes we are using an inline if .. if you don’t like it, pretend we didn’t)
1
|
|
Using what we know, the following should work:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Implementing unless
is left as an exercise for the reader :)
Trying it out
Putting the above into a Scala REPL (you will probably need to use :paste
mode) will prove that it works.
For example:
1 2 3 4 5 |
|
Also, remember that since our when
is backed by a macro, the f
argument (our block) passed to the second parameter list, behaves “lazily” and won’t execute if our predicatep
returns false. This is because when when
is invoked, the compiler knows to pass the entire AST for that block parameter (well, wrapped inside a c.Expr
) to our macro, which interpolates the it into the final tree.
For the performance-conscious, this means that we get “lazy” for free; that is, without using Scala’s call-by-name parameter feature, which, although nice to use in many cases, does incur some run-time performance penalty because it is implemented by instantiating anonymous classes (see this paper for more information about the performance cost of call-by-name parameters .. among other performance-related Scala things).
unless-when library
I’ve put the above into a library and included trailing variants of when
and unless
as bonuses (Rubyists should be familiar with these).
You can find the lib here on Github. It is fully tested and Mavenised for easy out-of-the-box usage.
Conclusion
I hope this post has been helpful in giving a simple, but full example of how to get started with macros in Scala. If you spot any errors, have questions or suggestions, please feel free to leave a comment!