Implement Ruby's attr_accessor with Scala's macros

28 Sep 2015

Note: this is to show the power of macros. If you need this feature, just use a case class.

As an exercise to learn macros, I have been trying to implement Ruby's attr_accessor with Scala's macros. I have felt that it can be done. It took me a while, but, yes, it can be done.

First of all, here is how we can use it:

class TestAttr { @attr_accessor var name: String = "yes" } val t = new TestAttr t.setName("Test") println(t.getName()) // "Test" t.setName("Test2") println(t.getName) // "Test2" t.name // Error occurs because the val name doesn't exist anymore

Here's the macros code:

import scala.annotation.StaticAnnotation import scala.collection.immutable.:: import scala.language.experimental.macros import scala.reflect.macros.blackbox.Context class attr_accessor extends StaticAnnotation { def macroTransform(annottees: Any*) = macro AttrAccessor.impl } object AttrAccessor { def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ val methods = annottees.map(_.tree).toList match { case (param: ValDef) :: rest if param.mods.hasFlag(Flag.MUTABLE) => val varName = param.name.toString val privateVarName = TermName(c.freshName("attrAccessor$")) List( ValDef( Modifiers(Flag.PROTECTED | Flag.MUTABLE, typeNames.EMPTY, List()), privateVarName, param.tpt, param.rhs ), { val firstArg = TermName(c.freshName("attrAccessor$")) DefDef( Modifiers(), TermName(s"set${varName.capitalize}"), List(), List( List( ValDef( Modifiers(Flag.PARAM, typeNames.EMPTY, List()), firstArg, param.tpt, EmptyTree ) ) ), TypeTree(typeOf[Unit]), Block( reify { println(s"Call set${c.Expr[Any](Literal(Constant(varName.capitalize))).splice}(${c.Expr[Any](Ident(firstArg)).splice})") }.tree, Assign(Ident(privateVarName), Ident(firstArg)) ) ) }, DefDef( Modifiers(), TermName(s"get${varName.capitalize}"), List(), List(List()), param.tpt, Block( reify { println(s"Call get${c.Expr[Any](Literal(Constant(varName.capitalize))).splice}()") }.tree, Ident(privateVarName) ) ) ) case other => throw new Exception(s"Expect ValDef with Flag.MUTABLE (or a var) but found $other") } c.Expr[Any](Block(methods, Literal(Constant(())))) } }

The hardest thing about this is to make it pass the compilation. The documentation on Scala's macros is pretty minimal.

Basically, what the above does is:

  1. Read val name: String = "yes"
  2. Declare the ValDef for protected var attrAccessor$privateVar = "yes"
  3. Declare the DefDef for
def setName(attrAccessor$firstArg: String): Unit = { println(s"Call setName(${attrAccessor$firstArg})") attrAccessor$privateVar = attrAccessor$firstArg }
  1. Declare the DefDef for:
def getName(): String = { println(s"Call getName()") attrAccessor$privateVar) }

That's it.

Again, the hard part is to just get everything compiled (e.g. get the right type into the right place). Once I made through the compilation, it works.


From the reddit's comment, I was suggested that the whole thing would be shorter with quasiquotes. It is much shorter. Here is the quasiquotes version:

object AttrAccessor { def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ val methods = annottees.map(_.tree).toList match { case (param: ValDef) :: rest if param.mods.hasFlag(Flag.MUTABLE) => val varName = param.name.toString val privateVarName = TermName(c.freshName("attrAccessor$")) val setterTermName = TermName(s"set${varName.capitalize}") val getterTermName = TermName(s"get${varName.capitalize}") List( q"private[this] var $privateVarName = ${param.rhs}", q""" def $setterTermName(value: ${param.tpt}) = { print(${Literal(Constant(s"$setterTermName("))}) print(${Ident(TermName("value"))}) println(${Literal(Constant(s")"))}) $privateVarName = value } """, q""" def $getterTermName() = { println(${Literal(Constant(s"$getterTermName()"))}) $privateVarName } """ ) case other => throw new Exception(s"Expect ValDef with Flag.MUTABLE (or a var) but found $other") } c.Expr[Any](Block(methods, Literal(Constant(())))) } }

Give it a kudos