Sometimes it is useful to be able to explore the parent AST in a macro. Examples include guiding type inference, building tree-like DSLs, etc (see below). Unfortunately, the current macro API makes it extremely awkward (also see below). Here I propose a way to separate such functionality in a well-defined (safe, optimized) new type of macros and leverage existing Scala features (traits + implicits) to take care of the rest.
Consider the following piece of Macroid layout:
l[LinearLayout](
w[TextView] ~> text("Hi"),
w[Button] ~> layoutParams(WRAP_CONTENT, WRAP_CONTENT, 1.0f)
) ~> vertical
Here layoutParams
macro needs to construct an object of class LinearLayout.LayoutParams
.
This means, we have to look up the layout tree to find the nearest l[X]
declaration and extract the X
.
In Macroid widgets can be tweaked with “Tweak
s” — mutating functions:
w[TextView] ~> text("Hi")
However, tweaking also supports functors:
List(w[TextView], w[Button]) ~> Option(text("Hi"))
This is achieved through the use of a typeclass TweakableWith[W, T]
(implementation).
Sometimes we don’t have a tweak like text
at hand, so we want to use widget’s methods:
w[Button] ~> tweak[Button] { x ⇒
x.doButtonishStuff1()
x.doButtonishStuff2()
}
As you can see, tweak[Button]
needs an explicit type specification.
The type can be inferred neither from the block of “buttonish” code, nor
from the context, because we use a typeclass to tie the sides of ~>
together (see implementation for details).
// does not work
// w[Button] ~> tweak { x ⇒ x.doButtonishStuff() }
If tweak
could look up the AST, it would see, that we are dealing with a Button
.
Looking up the AST is somewhat dangerous and ties macros to the surrounding context, hindering compilation. What if we could isolate it in a dedicated macro? Let’s try this with the second example.
trait WidgetType {
type W <: View // every Android widget extends View
}
// infer widget type
// since we can’t depend on wtp.W in the first argument,
// we have to redirect it to another class (TweakMaker)
def tweak(implicit wtp: WidgetType) = new TweakMaker[wtp.W]
// provide widget type explicitly
def tweak[W <: Widget] = new TweakMaker[W]
class TweakMaker[W <: View] {
def doing(f: W ⇒ Unit) = Tweak(f)
}
implicit def inferWidgetType: WidgetType = macro ???
Great! Now we have isolated widget type inference in inferWidgetType
, and we don’t even need to make tweak
a macro.
inferWidgetType
will observe its surroundings and return new WidgetType { type W = ... }
.
The usage is like this:
w[Button] ~> (tweak doing { x ⇒
x.doButtonishStuff1()
x.doButtonishStuff2()
})
// or
w[Button] ~> (tweak [Button] doing { x ⇒
x.doButtonishStuff1()
x.doButtonishStuff2()
})
// or
val myButtonTweak = tweak [Button] doing { x ⇒
x.doButtonishStuff1()
x.doButtonishStuff2()
})
w[Button] ~> myButtonTweak
We can use the same idea to implement the second example:
trait LayoutType {
type L <: ViewGroup // every Android layout extends ViewGroup
}
implicit def inferLayoutType: LayoutType = macro ???
def layoutParams(args: Any*)(implicit ltp: LayoutType): Tweak[ltp.L] = macro ???
See also a minimal working implementation of the first example.
Let’s see what we can get from the suggested approach.
The current way to implement, say, inferWidgetType
, is like this:
def inferWidgetTypeImpl(c: BlackboxContext) = {
import c.universe._
val tilde: PartialFunction[Tree, Type] = { case q"$x ~> $y" ⇒ c.typeCheck(x).tpe }
val tp = fromImmediateParentTree(c)(tilde).getOrElse(typeOf[View])
// `tp` could also be `List[Option[Button]]` etc.
// in that case we would need to dig to the inner type,
// but I will omit this for simplicity
q"new WidgetType { type W = $tp }"
}
/** THE HACK! */
def fromImmediateParentTree[A](c: BlackboxContext)(parent: PartialFunction[c.Tree, A]) = {
import c.universe._
// a parent contains the current macro application
def isParent(x: Tree) = parent.isDefinedAt(x) & x.find(_.pos == c.macroApplication.pos).isDefined
// an immediate parent is a parent and contains no other parents
c.enclosingImpl.find { x ⇒
isParent(x) && x.children.forall(_.find(isParent).isEmpty)
} map { x ⇒
parent(x)
}
}
As you can see, it’s not that nice. However, considering that our macro has one and only goal of observing the parent AST, we could give it extra capabilities. I suggest the following:
- Mark
WidgetType
or any trait, returned by an observer macro, with a special marker trait - Observer macros can only materialize such traits
- Observer macros can request
ObservingContext
, which has additional operations to navigate the AST up from the call site. This could be done in a form of a zipper, for example
Now, places, where looking up the AST is done, are isolated and well defined. Observer macros / materializers can be easily reused, and by employing them we can turn other macros into blackbox instead of whitebox, or even replace some of them with non-macro methods.