Skip to content

Instantly share code, notes, and snippets.

@stanch
Last active January 2, 2016 17:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stanch/8338601 to your computer and use it in GitHub Desktop.
Save stanch/8338601 to your computer and use it in GitHub Desktop.

The problem

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.

Motivating example 1

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.

Motivating example 2

In Macroid widgets can be tweaked with “Tweaks” — 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.

The idea

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.

Further thoughts

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment