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 :-)).