Type-safe dynamic beans factory with Scala 2.10 macros + Dynamic
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]
}