Macros in Julia, denoted by the @ prefix, are used to transform code before it is executed. They are often used to reduce boilerplate or implement domain specific languages (DSLs).
In a way, they are just normal functions which take in an abstract syntax tree or AST and return a different one, and their execution happens before that of all the normal code.
While functions run inside-out, macros run outside-in. When you execute outer(inner(x)) then the inner function runs first, and outer takes in what inner outputs. But if you execute @outer(@inner(x)), then @outer runs first, only after which any remaining macros inside the AST it outputs are run. In this example could still be @inner depending on what @outer outputs, but it doesn’t have to be. For example, there could be a @remove_macros macro, which just deletes any macro calls inside its body.
Macros don’t compose
One problematic consequence of this is that macros do not really compose. For example, let’s say you have two macros that operate on function definitions and add some useful things to them.
Let’s make one which wraps the body of a function in a timing operation. Note that I do this with an inner function as a quick-and-dirty way, because otherwise I have to deal with possibly multiple return statements from the function body.
macrofunctime(expr) expr.head ==:function||error("Not a function expression.") funcname = expr.args[1].args[1]:(function$(esc(funcname))(args...; kwargs...) f =$exprprintln("Started execution at $(time())") result =f(args...; kwargs...)println("Stopped execution at $(time())")return resultend )end@functimefunctionfunc()sleep(0.5)return"result"endfunc()
Started execution at 1.714752755314152e9
Stopped execution at 1.714752755823732e9
"result"
And here’s one that just logs that the function is being run:
macrofunclog(expr) expr.head ==:function||error("Not a function expression.") funcname = expr.args[1].args[1]:(function$(esc(funcname))(args...; kwargs...) f =$expr@info("Running function.") result =f(args...; kwargs...)return resultend )end@funclogfunctionfunc2()sleep(0.5)return"result"endfunc2()
[ Info: Running function.
"result"
But you cannot use both macros at once on a single function definition, because each macro expects an expression in form of a function definition as its argument. And putting a different macro inside means that the expression is of type :macrocall and not type :function, which our macros don’t know how to deal with.
Of course we can use higher-order functions for what I’m showing here, but that’s not the point of the exercise, it’s to try and see if we can use macros in a layered / composed way.
The inside-out macro
What I wanted to try here was to make the macros run from inside-out, like functions. For this, I made another small meta-macro which calls macroexpand from the inside out if it encounters multiple macros (with recursive = false because we want to keep any macros inside the main body intact throughout the transformations like usual). That means @insideout @macro1 @macro2 expr first expands @macro2 expr and then @macro1 output_expr.