Scalaxy/MacroExtensions: a DSL / compiler plugin to write macro-based DSLs (enrichments without runtime dependency!)
Quick link: Scalaxy/MacroExtensions on GitHub
Scala’s enrich-my-library pattern allows Scala developers to add methods (and more) to existing classes.
implicit def Extensions(self: Any) = new Extensions(self)
class Extensions(self: Any) {
def quoted(quote: String) = quote + self + quote
}
println(10.quoted("'")) // prints "'10'"
Scala 2.10 facilitates this pattern by providing implicit classes:
implicit class Extensions(self: Any) {
def quoted(quote: String) = quote + self + quote
}
println(10.quoted("'")) // prints "'10'"
This works great, but…
- It usually implies some object creation at runtime (lightweight though it might be, expecially if the extension is a value class),
- Your extension library is a new runtime dependency (which is not such a bad thing, but it can be avoided, please read on).
The other day, Eric Christiansen rightly complained on NativeLibs4Java’s mailing list that Scalaxy/Compilets were quite constrained by the typer, and that he wished he could define extension methods more easily.
I gave that a serious thought, and came up with a compiler plugin that runs before the typer / namer and performs the following expansion:
-
Input.scala:
@extend(Any) def quoted(quote: String) = quote + self + quote
-
Output.scala:
// Transformed AST after the compiler plugin's phase: implicit class quoted(self: Any) { def quoted(quote: String) = quote + self + quote }
Quite exciting syntax twisting, but overall not a huge line-saver.
Then I realized there’s another pattern that could benefit from such rewrites: macros enrichments.
A macro enrichment?
A macro enrichment just extends the “enrich-my-library” pattern by implementing the extension method using a macro. As a result, the enrichment is “inlined” at compilation time, and there’s no runtime dependency nor overhead. See my recent experiments for examples of such macro enrichments:
Scalaxy/MacroExtensions just uses the exact same syntax as above to create all the implicit class / macro wiring necessary to implement the extension as a macro:
-
Input.scala:
@scalaxy.extension[Any] def quoted(quote: String) = quote + self + quote
-
Output.scala:
// Transformed AST after the `scalaxy-extensions` compilation phase: import scala.language.experimental.macros implicit class scalaxy$extensions$quoted$1(self: Any) { def quoted(quote$Expr$1: String) = macro scalaxy$extensions$quoted$1.quoted } object scalaxy$extensions$quoted$1 { def quoted(c: scala.reflect.macros.Context) (quote$Expr$1: c.Expr[String]): c.Expr[String] = { import c.universe._ val Apply(_, List(selfTree$1)) = c.prefix.tree val self$Expr$1 = c.Expr[Any](selfTree$1) reify({ val self = self$Expr$1.splice val quote = quote$Expr$1.splice quote + self + quote }) } }
Update(Feb 22th 2013): Updated the syntax to use @scalaxy.extend instead of @extend.
The plugin supports two kinds of body for extension methods: regular code, and macro.
- If the body looks like some regular method implementation, as above, it will wrap it in a reify call and will wrap all references to self and to the method parameters in splice calls.
- If the body is a macro implementation, it will put it verbatim in the resulting macro:
-
Input.scala:
@scalaxy.extension[Any] def quoted(quote: String): String = macro { // `c` is defined as the macro Context. // `quote` and `self` are in scope, defined as `c.Expr[Any]` and `c.Expr[String]`. println(s"Currently expanding ${c.prefix}.quoted(${quote.tree})") // `c.universe._` is automatically imported, so `reify` is in scope. reify({ // Make sure we're not evaluating quote twice, in case it's a complex expression! val q = quote.splice q + self.splice + q }) }
-
Output.scala:
// Transformed AST after the `scalaxy-extensions` compilation phase: import scala.language.experimental.macros implicit class scalaxy$extensions$quoted$1(self: Any) { def quoted(quote: String) = macro scalaxy$extensions$quoted$1.quoted } object scalaxy$extensions$quoted$1 { def quoted(c: scala.reflect.macros.Context) (quote: c.Expr[String]): c.Expr[String] = { import c.universe._ val Apply(_, List(selfTree$1)) = c.prefix.tree val self = c.Expr[Any](selfTree$1) reify({ // Make sure we're not evaluating quote twice, in case it's a complex expression! val q = quote.splice q + self.splice + q }) } }
If you find this cool, just give it a try! (and follow me on Twitter if you want to hear about my next experiments :-)).