Scala 2.10 – Macros Hands-on With "Method Alias"
Join the DZone community and get the full member experience.
Join For FreeWhere?
Last week I was presenting at JFokus (cool conference, give it a try), a really nice JVM-focused conference. Obviously I love the conference I co-organise with fellow JUG members Geecon, too but it’s always fantastic to be a participant/speaker rather than the organiser once in a while – then you actually can enjoy the talks and not run around arranging everything… :-)
After the last day of the conference there was a Stockholm Scala User group meetup which I’ve obviously had to go to :-) With a beer in hand we jokingly invented a @alias annotation that would allow you to setup aliases for symbolic methods. An example would be akka’s ! (scala api) that delegates to tell (java api). While I had nothing to do waiting for the plane to arrive at the airport, I hacked up a proof of concept macro that does this.
As a heads up, all the code is on github: https://github.com/ktoso/scala-macro-method-alias
What?
I came up with two syntaxes, the first one being more scala-like:
And another one, wich looks more java-like (due to the annotation ;-)), and we’ll in the joke we said it should be an annotation:
Also it has been a great occasion to try out how implementing macros actually feels – and I gotta say, we’re not in lisp heaven here, but it’s pretty ok once you get a feel for it. Static typing for the win I’d say. Also, in 2.11 we’ll probably get a “quasiquoting string interpolator”, which would make creating Trees even easier.
In this post we’ll look at how implementing a macro in Scala 2.10 looks like. We’ll just cover the 1st syntax mentioned here (1st image).
How?
Let’s start by looking at the code then, shall we? On the call site we don’t have much to do. We just import the macro, and call it with the method (partially applied). The macro will then look at this method’s signature and delegate all params to it.
import pl.project13.scala.macros.Alias.aliasFor def !!(a: String, b: Int) = aliasFor { manyParams _ }
Very well… Let’s move on to the implementation. It has to be an separately compilable object. The compiler will compile it, and call the macros implementation while it’s compiling the class in which our call site is. In this aliasFor method, we take the partially applied call, and “invoke” the macro (notice the macro keyword).
import language.experimental.macros // enable the macros language feature import reflect.macros._ object Alias { def aliasFor[P1, P2, T](delegate: (P1, P2) => T): T = macro alias_impl[T] // ... }
Now it’ll get a bit more interesting. Let’s start with looking at line 1 – the macro’s def .
It’s closely related to the invocation of the macro, see the delegate parameter here? It must be named the same way as the delegate parameter in aliasFor.
If it isn’t or the signature here has more/less parameters than the
signature of aliasFor – you’ll get a compile error, informing you that
those must match.
Still in the signature, let’s look at c: Context, where’d that come from? You probably know – it’s the AST of our “surroundings”. You can call c.enclosingMethod or c.enclosingClass to get Tree‘s back from it. Also, it contains c.universe which may be thought of like “THE Scala entry point to reflection-land”, but since we’re still in compile-time, we get a compile-time universe. Using it we can create new AST nodes, or inspect anything that’s reachable from our code. It’s a big topic to wrap your head around, so it’s best to see the docs about it.
Next we’ll use this c to obtain what the method name of the one calling us is, and we can parse delegate to obtain the name of the method we want to delegate to. It will actually have the type information associated with it there by the way. Next we extract parameters that the delegator method has, and in the end, parse a small piece of scala code which basically is just the invocation of the target method + all the parameters it needs.
def alias_impl[T](c: Context)(delegate: c.Expr[Any]): c.Expr[T] = { import c._ val tree = delegate.tree.children match { case expr :: Nil => val methodName = extractDelegateMethodName(c) val params = extractDelegatorParams(c).mkString(", ") parse(s"""$methodName($params)""") // : c.Tree case _ => c.abort(c.enclosingPosition, "alias macro should only be used on single method delegation") } c.Expr(tree) }
private def extractDelegateMethodName(c: Context)(children: List[c.universe.Tree]): c.universe.Tree = children .map(a => a.children) .flatten .filter(_.isTerm) // PS: yeap I know this is getting ugly/long .filterNot(a => a.isEmpty && a.toString == "aliasFor") // exclude the macro name .head // first tree inside our macro .children.head // the thing inside is partially applied - it should be just one method
Notice that if someone would pass in more than one partially applied method into our aliasFor method, we would go into the _ case in here, and abort execution. The c.abort method causes the compiler to stop, and print out the position where it failed, along with the message we supply (there’s an example failure on the github of this project).
Lastly, having a Tree we need to convert it to an Expr – as that’s what a macro should return. Exprs are bound to an universe, but since in compile time, there’s only one universe – we can go ahead with using this simple c.Expr factory (it wouldn’t be that easy in runtime).
Sources?
That’s it… a quick overview on how writing macros looks with scala. I definitely recommend reading up about Universes and Def Macros before you head out and rewrite everything to be a macro ;-) As for the use case of this project… Well, in theory it’s useful, but the win is rather small – so I wouldn’t advertise using it everywhere it may fit ;-) It is on maven central if you’re interested to try it out though.
All usage information and sources sources (available under the Apache 2 License) are on GitHub, so check it out:
https://github.com/ktoso/scala-macro-method-alias
Published at DZone with permission of Konrad Malawski, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Top 10 Pillars of Zero Trust Networks
-
You’ve Got Mail… and It’s a SPAM!
-
Tomorrow’s Cloud Today: Unpacking the Future of Cloud Computing
-
Testing Applications With JPA Buddy and Testcontainers
Comments