DAYS 2013
Using Macros to Inline Anonymous Functions
Erik Osheim (@d6)
This talk is dedicated to Iain Banks
16 February 1954 - 9 June 2013
What are macros?
Macros are hygenic, compile-time code transformations..
In 2.10, macros are defined as methods which take parameters as ASTs (abstract syntax trees), and return a result as an AST.
What are anonymous functions?
Anonymous functions are values that can be called (like methods). For example:
ns.map((x: Int) => x + 6) ns.map(x => x + 6) ns.map(_ + 6) def plusSix(n: Int): Int = n + 6 ns.map(plusSix)
(What's different about the last one?)
What was different?
The last application was equivalent to:
def plusSix(n: Int): Int = n + 6 ns.map(x => plusSix(x))
This anonymous function really just wraps a (non-anonymous) method.
What is inlining?
In our case, inlining means rewriting an application with its implementation.
def addOne(n: Int): Int = n + 1 def timesTwo(n: Int): Int = n * 2 def minusThree(n: Int): Int = n - 3 addOne(timesTwo(minusThree(6))) def allOfIt(n: Int): Int = ((n - 3) * 2) + 1 allOfIt(6)
So what does it all mean together?
We want to write methods that accept literal anonymous functions, and inline their implementations using macros.
def normal(n: Int, f: Int => Int): Int = f(n) normal(6, _ * 3) def secret(n: Int, f: Int => Int): Int = ??? secret(6, _ * 3) // compiles to 6 * 3
What are the limits?
We need a literal anonymous function that "does the work" or else we can't inline properly.
def secret(n: Int, f: Int => Int): Int = ??? def tripled(n: Int): Int = n * 3 secret(6, tripled) // nope! // compiles to tripled(6), not 6 * 3
Why would you want to do this?
There are many totally valid reasons to do something like this!
- Perversity
- Replacement for code generation
- Better specialization
- Premature Optimization
- Solving The Inlining Problem
What does a macro look like?
object Scope { def secret(n: Int, f: Int => Int): Int = macro sec def sec(c: Context) (n: c.Expr[Int], f: c.Expr[Int => Int]): c.Expr[Int] = { // transform input Exprs into an out Expr } }
What does an Expr (AST) look like?
Demo.secret(6, _ * 3) // becomes (approximately) Apply( // call Demo.secret method Select(Demo, secret), List( // 2 arguments Literal(Constant(6)), Function( // anonymous function List(x), // one parameter named x Apply( // call x.+ Select(x, +), List( // 1 argument: Literal(Constant(3)))))))
What does an Expr (AST) look like?
There is a bit more complexity regarding names:
Select( This(newTypeName("Demo")), newTermName("secret")) Select( Ident(newTermName("x$1")), newTermName("$plus"))
So what does an inlining transformation look like?
- Verify arguments are "valid"
- Inline anonymous function body into tree
- Inline arguments for parameters
- Clean up
So what does an inlining transformation look like?
def normal(n: Int, f: Int => Int): Int = f(n) // user calls the macro, verify args Demo.secret(6, (x: Int) => x * 3) // start with f.apply(n) ((x: Int) => x * 3).apply(6) // remove apply, inline f's body x * 3 // substitute 6 (argument) for x (parameter) 6 * 3
What do you notice about this?
What do you notice about this?
- It looks fiddly
- Between steps 2-4 the tree is invalid
- Clean up was not mentioned
- Scope problems?
How about the actual AST transformations?
Demo.secret(6, (x: Int) => x * 3)
How about the actual AST transformations?
Apply( Select(This(newTypeName("Demo")), newTermName("secret")), List( Literal(Constant(6)), Function( List(ValDef( Modifiers(PARAM | SYNTHETIC), newTermName("x$1"), TypeTree(), EmptyTree)), Apply( Select(Ident(newTermName("x$1")), newTermName("$plus")), List(Literal(Constant(3)))))))
How about the actual AST transformations?
Apply( Select( Function( List(ValDef( Modifiers(PARAM | SYNTHETIC), newTermName("x$1"), TypeTree(), EmptyTree)), Apply( Select(Ident(newTermName("x$1")), newTermName("$plus")), List(Literal(Constant(3))))), newTermName("apply")), List(Literal(Constant(6))))
How about the actual AST transformations?
Apply( Select(Ident(newTermName("x$1")), newTermName("$plus")), List(Literal(Constant(3))))
How about the actual AST transformations?
Apply( Select(Literal(Constant(6)), newTermName("$plus")), List(Literal(Constant(3))))
What clean up is necessary?
New trees and fragments need valid owners/types.
Use c.resetAllAttrs to "fix" generated trees.
What are some uses of inlining?
- Looping syntax without indirection cost
- Stream fusion
- Multiple return values
- Building parsers at compile-time
None of these applications are otherwise impossible,
but this strategy can make some approaches more attractive.
What are some problems?
- It's complicated
- Only works with literal values
- Violates intuition
- Requires more/different documentation