With Scala 2.10.0 just out, we’ve now got a couple of shiny new toys:

  • Macros provide a clean (but experimental) way to plug into the Scala compiler, making AST rewrites very easy and mostly type-safe.
  • Dynamic is a type that lets you hijack method calls to predefined “fallback” methods (applyDynamic and al.).

The very interesting thing here is that despite its name, Dynamic only involves compile-time routing of unknown methods… and it is possible to implement applyDynamic using a macro!

This means using Dynamic you can create DSLs with code that is loosely typed (in Scala type system terms) and with macros we can still add your own custom compilation-time type-checks, and even rewrite the code so that it no longer refers to Dynamic at the first place!

Some practice: a type-safe dynamic beans factory

Java Beans are a bit tedious to create from Scala:

{
  val bean = new MyBean
  bean.setFoo(10)
  bean.setBar(12)
  bean
}

Well, with a Dynamic subclass that implements applyDynamicNamed using a macro, we can reach the following syntax:

beans.create[MyBean](
  foo = 10,
  bar = 12
)

/*
UPDATE: the latest code in github (see link below) implements
the following, more flexible and friendly syntax:

new MyBean().set(
  foo = 10,
  bar = 12
)
*/

And of course, this is all type-checked and rewritten to the exact same setter-intensive code as above, with the exact same runtime performance (nifty, isn’t it?).

Enjoy! (see newest sources and tests in Scalaxy’s repository)

package scalaxy

import scala.language.dynamics
import scala.reflect.NameTransformer
import scala.reflect.macros.Context

/**
  Syntactic sugar to instantiate Java beans with a very Scala-friendly syntax.
  (full sources and tests: https://github.com/ochafik/Scalaxy/tree/master/Beans)

  The following expression:

    import scalaxy.beans

    beans.create[MyBean](
      foo = 10,
      bar = 12
    )

  Gets replaced (and type-checked) at compile time by:

    {
      val bean = new MyBean
      bean.setFoo(10)
      bean.setBar(12)
      bean
    }

  Doesn't bring any runtime dependency (macro is self-erasing).
  Don't expect code completion from your IDE as of yet.
*/
object beans extends Dynamic
{
  def applyDynamicNamedImpl[R : c.WeakTypeTag]
    (c: Context)
    (name: c.Expr[String])
    (args: c.Expr[(String, Any)]*) : c.Expr[R] =
  {
    import c.universe._

    // Check that the method name is "create".
    name.tree match {
      case Literal(Constant(n)) =>
        if (n != "create")
          c.error(name.tree.pos, s"Expected 'create', got '$n'")
      case _ =>
        c.error(name.tree.pos, "Unexpected name structure error")
    }

    // Get the bean type.
    val beanTpe = weakTypeTag[R].tpe

    // Choose a non-existing name for our bean's val.
    val beanName =
      newTermName(c.fresh("bean"))

    // Create a declaration for our bean's val.
    val beanDef =
      ValDef(Modifiers(), beanName, TypeTree(beanTpe), New(TypeTree(beanTpe), Nil))

    // Try to find a setter in the bean type that can take values of the type we've got.
    def getSetter(name: String, valueTpe: Type, valuePos: Position) = {
      beanTpe.member(newTermName(name)).filter {
        case s =>
          s.isMethod && (
            s.asMethod.paramss.flatten match {
              case Seq(param) =>
                // Check that the parameter can be assigned a convertible type.
                // (typeOf[Int] weak_<:< typeOf[Double]) == true.
                if (!(valueTpe weak_<:< param.typeSignature))
                  c.error(valuePos, s"Value of type $valueTpe cannot be set with $beanTpe.$name(${param.typeSignature})")
                true
              case _ =>
                false
            }
          )
      }
    }

    // Generate one setter call per argument.
    val setterCalls = args.map(_.tree).map 
    {
      // Match Tuple2.apply[String, Any](fieldName, value).
      case Apply(_, List(n @ Literal(Constant(fieldName: String)), v @ value)) =>

        // Check that all parameters are named.
        if (fieldName == null || fieldName == "")
          c.error(v.pos, "Please use named parameters.")

        // Get beans-style setter or Scala-style var setter.
        val setterSymbol =
          getSetter("set" + fieldName.capitalize, value.tpe, v.pos)
            .orElse(getSetter(NameTransformer.encode(fieldName + "_="), value.tpe, v.pos))

        if (setterSymbol == NoSymbol)
          c.error(n.pos, s"Couldn't find a setter for field '$fieldName' in type $beanTpe")

        Apply(Select(Ident(beanName), setterSymbol), List(value))
    }
    // Build a block with the bean declaration, the setter calls and return the bean.
    c.Expr[R](Block(Seq(beanDef) ++ setterCalls :+ Ident(beanName): _*))
  }

  def applyDynamicNamed[R](name: String)(args: (String, Any)*): R =
    macro applyDynamicNamedImpl[R]
}