The most horrible code I've ever written
When macros first showed up in Scala as an experimental language feature last year, many Scala developers responded with skepticism or distaste. They argued that macros were a distraction from work on more urgent problems with the language, that they would lead to even more complex and reader-unfriendly code, etc.
After a year and a half I think these arguments have less weight, as macros have proven extremely useful in a wide range of applications: string interpolation, serialization, type-level programming with singletons, numeric literals and faster loops, typed channels for actors, and so on. They've let me make many parts of my own code faster and safer in surprising ways.
This post is not about a useful application of macros. It's inspired by a couple of questions on Stack Overflow today, and is an example of exactly the kind of thing macros should not ever be used for. But it's Friday evening and I'm drinking beer in the office and I think this trick is pretty clever, so here we go.
Suppose we've got a Scala class with some mutable fields:
case class Car(var speed: Int, var color: String, var position: Int = 0)
Also suppose we want to be able to write something like this:
val car = new Car(0, "blue")
car set {
color = "red"
speed = 100
}
Apparently some kind of syntax along these lines is possible in charming languages like Groovy and Visual Basic.
We can get close in Scala by adding a set
method to Car
:
def set(f: (Car) => Unit) = f(this)
And now we can write the following:
car set { c =>
c.color = "red"
c.speed = 100
}
But suppose we're not happy about those nine or ten extra characters,
or that we don't want the overhead of the extra function application.
The problem is that color
and speed
don't mean anything on their own.
The argument to the macro needs to typecheck before the macro is expanded, so
we can't for example write a macro that would just prepend import car._
to
the block (as proposed here).
If we had untyped macros,
this would be easy, but they've been thrown out of paradise. We're not completely
out of luck with plain old def
macros, though, since it's possible to use them to
introduce structural types with arbitrarily named and typed methods.
This means
we can bring some dummy methods into scope that have the same shape as the
setters for Car
. The compiler will see these methods when it typechecks the macro
argument, and then our macro implementation can rewrite them to be calls to the
real setters on Car
.
Before we see my implementation of this horrible idea, let's make the problem harder.
Suppose we also have some methods on Car
that change its state:
case class Car(var speed: Int, var color: String, var position: Int = 0) {
def move(t: Int) = position += t * speed
}
And want to be able to call these in our set
block, like this:
car set {
color = "red"
move(13)
}
This is so incredibly unpleasant I can't believe I'm even writing it.
I'll start with the easy part—the macro implementation of set
. Note that
I'm using quasiquotes, which are now available as a plugin
in Scala 2.10. Without them this post would fill a small book.
import scala.reflect.macros.Context
import scala.language.experimental.macros
trait SetterBuilder {
def set_impl(c: Context)(assignments: c.Expr[Unit]): c.Expr[Unit] = {
import c.universe._
val rewriteOne: PartialFunction[Tree, Tree] = {
case q"${_}.$n($v)" => q"${c.prefix}.$n($v)"
}
val rewrite: PartialFunction[Tree, Tree] = rewriteOne orElse {
case block: Block => q"{ ..${block collect rewriteOne} }"
}
c.Expr(
rewrite.lift(assignments.tree).getOrElse(
c.abort(c.enclosingPosition, "Not a set of assignments!")
)
)
}
}
Pretty straightforward. We just check the argument for assignments and try to rewrite them as assignments on the macro's prefix.
Next for the messier part—building the instance of the structural type that will give us our dummy methods:
trait SyntaxBuilder {
def syntax_impl[A: c.WeakTypeTag](c: Context) = {
import c.universe._
val anon = newTypeName(c.fresh())
val declarations = c.weakTypeOf[A].declarations
val (getters, setters) = declarations.collect {
case sym: MethodSymbol if sym.isSetter => (
q"def ${sym.getter.name} = ???",
q"def ${sym.name}(x: ${sym.paramss.head.head.typeSignature}) = ???"
)
}.unzip
val others = declarations.collect {
case sym: MethodSymbol if sym.returnType =:= typeOf[Unit] && !sym.isSetter =>
val params = sym.paramss.map(
_.map(
p => ValDef(
Modifiers(Flag.PARAM),
p.name.toTermName,
TypeTree(p.typeSignature),
EmptyTree
)
)
)
DefDef(Modifiers(), sym.name, Nil, params, TypeTree(), reify(???).tree)
}
c.Expr[Any](q"class $anon { ..$getters; ..$setters; ..$others }; new $anon {}")
}
}
This wouldn't be quite so bad if I could be bothered to figure out how to quasiquote multiple parameter lists; at the moment I can't be.
Now we tie these pieces together:
object Evil extends SyntaxBuilder with SetterBuilder {
def syntax[A] = macro syntax_impl[A]
}
And then define our Car
class:
case class Car(var speed: Int, var color: String, var position: Int = 0) {
def move(t: Int) = position += t * speed
def set(assignments: Unit): Unit = macro Evil.set_impl
}
object Car {
val syntax = Evil.syntax[Car]
}
import Car.syntax._
And we're done:
scala> val car = new Car(0, "blue")
car: Car = Car(0,blue,0)
scala> car set {
| color = "red"
| speed = 10
| move(13)
| speed = 1
| move(42)
| }
scala> car
res0: Car = Car(1,red,172)
It's so ridiculous. I love it.