(The source code you will find below is expected to be compiled on Scala 2.10-M7. Note, that -M6 provides a slightly different set of API for macro.)
Lets define a macro that makes it possible to traverse value fields of a (case) class. First, we import what we will use:
import language.experimental.macros import scala.reflect.macros.Context import scala.annotation.AnnotationThe macros, we are implementing, will be located in 'annotated' object since Scala allows usage of type aliases inside object (unlike package namespace).
object annotated {Any field belongs to a class (denoted as I). An annotated field might have useful information given by arguments on annotation. Type of the annotation arguments is denoted as A. Here is the definition of the Field class:
/** * An object of this class represents an annotated field. * @tparam I type of class the field belongs to * @tparam A type of annotation arguments (TupleX or None) * @param name name of the field * @param get function that returns field value of an instance given as argument to the function * @param args list of arguments to the annotation found on the field */ case class Field[I <: AnyRef, A <: Product](name : String, get : I => Any, args : A)Here is the type alias to save some typing:
/** * List of fields belonging to the given type. * @tparam I Owner of fields * @tparam A type of annotation arguments (TupleX or None) */ type Fields[I <: AnyRef, A <: Product] = List[Field[I, A]]That is how our macro is supposed to be seen by developers (i.e. it is supposed to be seen as an ordinary function):
/** * Macro which inspects class 'I' and returns a list of fields annotated with annotation 'Ann'. * @tparam Ann search for field with this annotation * @tparam Args type of arguments in the annotation (TupleX or None) * @tparam I type of class to scan for annotated fields */ def fields[Ann <: Annotation, Args <: Product, I <: AnyRef] = macro fieldsImpl[Ann, Args, I]Finally, here is the implementation of the macro itself. The implementation is called by the scala compiler whenever it sees 'fields' macro:
/** * Implementation of the fields macro. */ def fieldsImpl[AnnTT <: Annotation : c.AbsTypeTag, Args <: Product : c.AbsTypeTag, ITT <: AnyRef : c.AbsTypeTag](c : Context) : c.Expr[Fields[ITT, Args]] = {("Args <: Product : c.AbsTypeTag" means "type Args is a subtype of type Product and there is an implicit value of type c.AbsTypeTag[Args]")
Note that here and further below we use types (like AbsTypeTag) which are from the context 'c'. That is a compilation context of the application which the scala compiler will construct for the source code where the macro invocation is faced (not exactly but..).
Now lets import types and values (like Select, Ident, newTermName) from the universe of the application the macro is currently used in:
import c.universe._
Lets materialize some types as objects for further manipulation:
val instanceT = implicitly[c.AbsTypeTag[ITT]].tpe val annT = implicitly[c.AbsTypeTag[AnnTT]].tpeNow, some real action: get annotated fields. Note that hasAnnotation doesn't work for a reason I don't know...
val annSymbol = annT.typeSymbol val fields = instanceT.members filter (member => member.getAnnotations.exists(_.atp == annT))It is convenient to have a helper function. This function will fold given expression sequence into a new expression that creates List of expressions at runtime ;-)
def foldIntoListExpr[T : c.AbsTypeTag](exprs : Iterable[c.Expr[T]]) : c.Expr[List[T]] = exprs.foldLeft(reify { Nil : List[T] }) { (accumExpr, expr) => reify { expr.splice :: accumExpr.splice } }For each field, construct expression that will instantiate Field object at runtime:
val fieldExprs = for (field <- fields) yield { val argTrees = field.getAnnotations.find(_.atp == annT).get.args val name = field.name.toString.trim // Why is there a space at the end of field name?! val nameExpr = c literal name // Construct arguments list expression val argsExpr = if (argTrees.isEmpty) { c.Expr [Args] (Select(Ident(newTermName("scala")), newTermName("None"))) } else { val tupleConstTree = Select(Select(Ident(newTermName ("scala")), newTermName(s"Tuple${argTrees.size}")), newTermName("apply")) c.Expr [Args] (Apply (tupleConstTree, argTrees)) } // Construct expression (x : $I) => x.$name val getFunArgTree = ValDef(Modifiers(), newTermName("x"), TypeTree(instanceT), EmptyTree) val getFunBodyTree = Select(Ident(newTermName("x")), newTermName(name)) val getFunExpr = c.Expr[ITT => Any](Function(List(getFunArgTree), getFunBodyTree)) reify { Field[ITT, Args](name = nameExpr.splice, get = getFunExpr.splice, args = argsExpr.splice) } }By this moment, value fieldExprs will contain something like
List ( reify {Field ('field1', (x => x.field1), (..)}, reify {Field ('field2', (x => x.field2), (..)} )(where (..) are arguments of annotaiton on that field)
Now we have to lift List construction into expression and we're done!
// Construct expression list like field1 :: field2 :: Field3 ... :: Nil foldIntoListExpr(fieldExprs) } }
And finally lets have some fun. Lets test it in REPL! (beware that scala macros are supposed to be compiled before they are used)
scala> type FormatFun = Any => Any defined type alias FormatFun scala> type PrettyArgs = (Option[String], FormatFun) defined type alias PrettyArgs scala> class Pretty(aka : Option[String] = None, format : FormatFun = identity) extends annotation.StaticAnnotation defined class Pretty
scala> :paste // Entering paste mode (ctrl-D to finish) def pp[X <: AnyRef](fields : annotated.Fields[X, PrettyArgs])(x : X) = { fields map { case annotated.Field(fieldName, get, (akaOpt, fmtFun)) => val name = fieldName.replaceAll("([A-Z][a-z]+)", " $1").toLowerCase.capitalize val aka = akaOpt map (" (aka " + _ + ")") getOrElse "" val value = fmtFun(get(x)) s"$name$aka: $value" } mkString "\n" } // Exiting paste mode, now interpreting. pp: [X <: AnyRef](fields: info.akshaal.radio.uploader.annotated.Fields[X,(Option[String], Any => Any)])(x: X)String
Still no macro was used. Now here it comes. First, we define case class. Next, we gather annotated fields in the definition of personPrettyFields. When you run it in REPL, it is quite important to use :paste, otherwise annotation of the case class will be lost because subtypes of StaticAnnotation are visible during compilation only (REPL calls a new scala compiler for each expression reusing binary classes compiled during previous steps). So:
scala> :paste // Entering paste mode (ctrl-D to finish) case class Person( id : Int, @Pretty(aka = Some("nickname")) name : String, @Pretty firstName : String, @Pretty(None, format = _.toString.toUpperCase) secondName : String, @Pretty(None, format = { case x : Option[_] => x getOrElse "" }) twitter : Option[String]) val personPrettyFields = annotated.fields[Pretty, PrettyArgs, Person] // Exiting paste mode, now interpreting. defined class Person personPrettyFields: List[info.akshaal.radio.uploader.annotated.Field[Person,(Option[String], Any => Any)]] = List(Field(name,<function1>,(Some(nickname),<function1>)), Field(firstName,<function1>,(None,<function1>)), Field(secondName,<function1>,(None,<function1>)), Field(twitter,<function1>,(None,<function1>)))(I've aligned the output of REPL a bit..)
Lets check field names:
scala> personPrettyFields map (field => field.name) res0: List[String] = List(name, firstName, secondName, twitter)Here are getters of each field:
scala> personPrettyFields map (_.get) res1: List[Person => Any] = List(<function1>, <function1>, <function1>, <function1>)Now, lets create a Person object:
scala> val person1 = Person(1, "akshaal", "Evgeny", "Chukreev", Some("https://twitter.com/Akshaal")) person1: Person = Person(1,akshaal,Evgeny,Chukreev,Some(https://twitter.com/Akshaal))... and a value for each field of this person:
scala> personPrettyFields map (_.get (person1)) res2: List[Any] = List(akshaal, Evgeny, Chukreev, Some(https://twitter.com/Akshaal))Some more objects for more fun:
scala> val person2 = Person(2, "BillGates", "Bill", "Gates", Some("https://twitter.com/BillGates")) person2: Person = Person(2,BillGates,Bill,Gates,Some(https://twitter.com/BillGates)) scala> val persons = List(person1, person2) persons: List[Person] = List(Person(1,akshaal,Evgeny,Chukreev,Some(https://twitter.com/Akshaal)), Person(2,BillGates,Bill,Gates,Some(https://twitter.com/BillGates))) scala> val ppPerson = pp(personPrettyFields) _ ppPerson: Person => String = <function1>And finally:
scala> persons map ppPerson mkString "\n----------------------------\n" res5: String = Name (aka nickname): akshaal First name: Evgeny Second name: CHUKREEV Twitter: https://twitter.com/Akshaal ---------------------------- Name (aka nickname): BillGates First name: Bill Second name: GATES Twitter: https://twitter.com/BillGates
That's all ;-) You will find complete source code along with specs2 specification (with one more example) on the gist: https://gist.github.com/3388753
https://issues.scala-lang.org/browse/SI-5736 will provide some info about why there is a whitespace at the end of field names.
ReplyDeleteThanks for sharing this
ReplyDelete