Scala Macros have been part of the language since Scala 2.10. Let’s have some fun with them…
The full project accompanying this blog post is on github.
Consider this simple group of functions:
@evil
object Operations {
def addTwoNumbers(a: Int, b: Int): Int = a + b
def maxOfThreeNumbers(a:Int, b: Int, c: Int): Int = math.max(math.max(a, b), c)
def lengthOfString(s: String): Int = s.length
}
And associated simple tests:
import org.scalatest.{Matchers, WordSpec}
class OperationsSpec extends WordSpec with Matchers {
import Operations._
"Operations" should {
"Add Two Numbers" in {
addTwoNumbers(3, 4) should be (7)
}
"Find the max of three numbers" in {
maxOfThreeNumbers(12, 23, 20) should be (23)
}
"Find the length of a string" in {
lengthOfString("abc") should be (3)
}
}
}
Running these tests (with sbt core/test
) gives:
[info] OperationsSpec:
[info] Operations
[info] - should Add Two Numbers *** FAILED ***
[info] 8 was not equal to 7 (OperationsSpec.scala:12)
[info] - should Find the max of three numbers *** FAILED ***
[info] 24 was not equal to 23 (OperationsSpec.scala:16)
[info] - should Find the length of a string *** FAILED ***
[info] 4 was not equal to 3 (OperationsSpec.scala:20)
This does not bode well for the future of Scala! However, observant readers may have
noticed the @evil
annotation. An obvious culprit, let’s see what it does.
In the ‘macros’ sub project in the git repo, the @evil
annotation is defined as:
@compileTimeOnly("enable macro paradise to expand macro annotations")
class evil extends StaticAnnotation {
def macroTransform(annottees: Any*) = macro EvilMacro.impl
}
scala.annotation.StaticAnnotation
is a trait with no members. Extending StaticAnnotation
in this way defines an annotation with the same name as the class. The inclusion
of the method def macroTransform(annottees: Any*)
informs the Scala macro engine
that this annotation is a Macro annotation. The compileTimeOnly
annotation
on our custom annotation class ensures that the code will only work if we have
the macro paradise plugin enabled in our project.
While macros in general are supported natively in Scala, macro annotations require this plugin
in order to operate.
The result is that the annotation @evil
is made available, and wherever it is used,
the macro defined by EvilMacro.impl
will be executed during compilation.
The input to the Macro is the source tree of the artifact on which the annotation
was used. The Macro can alter the tree, therefore altering the code which results
from the compilation of the source.
Let’s take a look at the implementation of EvilMacro
:
import scala.reflect.macros.whitebox.Context
object EvilMacro {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
annottees match {
case List(annottee @ Expr(
mod@ModuleDef(m, n, Template(p, s, b)))) =>
//annottee.tree.children.foreach(println)
val newTemplateChildren = b.map{
case d @ DefDef(mods, name, types, params,
t @ Ident(TypeName("Int")), body) =>
val amendedBody = q"($body) + 1"
DefDef(mods, name, types, params, t, amendedBody)
case other => other
}
val amendedModule =
ModuleDef(m, n, Template(p, s, newTemplateChildren))
c.Expr(q"{$amendedModule}")
case other =>
Expr
println(s"Expecting single module")
c.Expr(Block(annottees.map(_.tree).toList, Literal(Constant(()))))
}
}
}
Implementing a macro requires a context, either blackbox or whitebox. The difference
is explained on this page
but macro annotations are required to be whitebox so we import scala.reflect.macros.whitebox.Context
.
An implementation of a macro is a curried function which accepts a context and a list
of expressions, and returns a new expression.
In this case we are implementing a macro specifically for annotating objects, so
we pattern match on the input being a list with a single member of type ModuleDef
.
It can take some trial and error to find the types representing the intended code tree
when first implementing macros. The unapply methods for types such as ModuleDef
are
contained in classes with names such as ModuleDefExtractor
. Their scaladoc can be found
here.
The third parameter of the ModuleDef
extractor
is a instance of Template
and its third parameter is the list of expressions
within the object definition. We then iterate over each expression, matching specifically
on instances of DefDef
(representing function definitions) with a return type of Int
.
We redefine the definition of the the body of each such function, using the expression
q"($body) + 1"
. The use of ‘q’ defines this as a quasiquote.
Quasiquote notation allows us to combine expressions with scala code strings
to form new expressions. In this case, we take whatever the existing body of the
function was, and add 1 to it. Then we reassemble the Template and ModuleDef.
Anything tagged with our @evil
annotation is subject to this transformation at
compile time, so our functions change their behaviour and our tests fail. Perhaps a good
way to make sure your colleagues are paying attention during code review?