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
/*
  This should replace if (true) { block of code }
*/
when (true) {
  // block of code
}

/*
  This should replace if (!true) { do something }
*/
unless (false) {
  // block of code
}

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
import scala.language.experimental.macros
import scala.reflect.macros._

object Example {
  def meth[A](x: A): A = macro implRef[A]

...

  def implRef[A: c.WeakTypeTag](c: Context)(x: c.Expr[A]): c.Expr[A] = ...
}

What does this mean? Let’s break it down:

  • import scala.language.experimental.macros and import 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 on scala-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 invoke meth with an Int, we expect the output to be an Int 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. The macro keyword lets the compiler know that the body of this method is going to be implemented via a macro definition, in this case implRef.
  • 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 part def 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 from meth into the implRef macro expansion implementation, allowing us to have a type parameterised macro definition.
    • (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) is x and it is a c.Expr[A]. It is important that the name of the parameter matches that used in the invoking method (see how meth also has x 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 method meth 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”.
    • : c.Expr[A] all this means is that the result of the macro expansion is also a c.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
scala> val aquasiquote = q"a quasiquote"
aquasiquote: universe.Select = a.quasiquote

scala> val tree = q"i am { $aquasiquote }"
tree: universe.Tree = i.am(a.quasiquote)

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
when (someCondition) { result }

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
if (someCondition) Some(result) else None

Using what we know, the following should work:

1
2
3
4
5
6
7
8
9
10
11
12
import scala.language.experimental.macros
import scala.reflect.macros._

object Example {

  def when[A](p: Boolean)(f: A): Option[A] = macro whenImp[A]

  def whenImp[A: c.WeakTypeTag](c: Context)(p: c.Expr[Boolean])(f: c.Expr[A]): c.Expr[Option[A]] = {
    import c.universe._
    c.Expr[Option[A]](q"if ($p) Some($f) else None")
  }
}

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
scala> import Example._
import Example._

when(true)(3)
res1: Option[Int] = Some(3)

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!

Comments