macro-paradise

Macro Annotated Tuple

Scala is a very expressive programming language. Most of time few lines are good enough to suit the programmer needs. However, it is not always possible to escape situations where boilerplate is unavoidable. For instance, I firmly believe that there must be a prettier implementation for Scala tuples (Tuple2, Tuple3, …, TupleN). After playing with the new metaprogramming features for a while, especially Macro Annotations from Macro Paradise, I found that these tools could be useful to reduce this kind of boilerplate. From now on, I will be telling you my modest attempt to deploy the minimal implementation for Scala tuples.

What’s the Problem?

Well, the problem is quite straightforward. This is annoying boilerplate:

class MyTuple1[A](val _1: A)

class MyTuple2[A, B](val _1: A, val _2: B)

...

class MyTupleN[A, B, ..., N](
  val _1: A,
  val _2: B,
  ...,
  val _n: N)

The involved pattern is very clear (MyTupleX requires X type parameters and X accessors) and that leads to a sequence of repetitive stuff. There’s no place for such an ugly code in a language which aims expressiveness.

Macro Annotations to the Rescue

Macro annotations from macro paradise are empowered to generate new types, in contrast with the classical def macros. This opens a huge range of new possibilities such as transforming the annotated type itself or even providing a brand new companion object for it, whose contents could depend on the original type. How can we use them in the tuple scenario? My approach consists of using a class as a kind of type pattern, which will conform the macro input. Then the macro will generate all the required Tuple types. This is easier to understand by showing the pattern:

@expand(30) class MyTupleα[Tα](val _α: Tα)

As you could imagine, expand is the name for the macro annotation. It takes a number as input, which corresponds with the number of types that we want to generate for the pattern (in this case MyTuple1, MyTuple2, …, MyTuple30). Then we can see the pattern, which uses an alpha to represent the variable positions. This character will be replaced in the sequence of types that will be expanded at compilation time.

I’ve just uploaded the sources to github. If you are used to def macros you will notice that the way to implement macro annotations is not very different, once you’ve declared the header:

class expand(val bound: Int) extends StaticAnnotation {
  def macroTransform(annottees: Any*) = macro Macros.expandImpl
}

In fact, this is just an annotation definition which contains a def macro inside. That definition needs to take a sequence of annottees. For our case, you can think of it as a sequence containing a single element: the type pattern.

The macro implementation is partially shown in the following snippet:

def expandImpl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
  import c.mirror._
  import c.universe._

  val q"new expand($b)" = c.prefix.tree
  val Literal(Constant(bound: Int)) = b

  // ...

  val classes = for {
    i   } yield q"""
    class ${newTypeName(name.decoded.replaceAll("α", i.toString))}
      [..${newtparams(i)}](..${newparams(i)}
    )"""

  c.Expr[Any](Block(classes, Literal(Constant(()))))
}

The most interesting thing to remark is undoubtly the use of quasiquotes (q”…”), both to parse the upper bound and generate the output classes. You may not be able to appreciate how amazing is this new feature unless you had to deal with the old manually generation of Scala ASTs. Really. Once we’ve made the new types, we just return them inside a block.

Conclusions

Macros are icebergs that hide the expansion encodings real size under the surface of the sea. In this case, we used them to generate a sequence of types whose size can be chosen by the macro user. By adopting this approach, the maintainability is clearly better.

Honestly, the current implementation to generate the tuples is very ad-hoc, but I think that the pattern idea could be generalised to deal with more complex structures than the one shown in this post. By doing so, the number of lines required to implement this kind of boilerplate, that we can find in Functions as well, would be reduced to a minimum.

Advertisements