Fundamentally, what is a macro? A macro transforms syntax into syntax, there’s no values involved here (in some sense, the values that will eventually be computed don’t exist during macro expansion at all - there’s only parsed syntax, none of the modified code has run yet). Let’s take a look at what we have (f(a,b)
and :mystruct
) and what we want (f(a, b*value(mystruct))
). What do these two look like once they’re parsed, i.e. what does the julia compiler see? We can find out via Meta.@dump
:
julia> Meta.@dump (f(a,b), mystruct)
Expr
head: Symbol tuple
args: Array{Any}((2,))
1: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol f
2: Symbol a
3: Symbol b
2: Symbol mystruct
This may look scary at first, so let’s break it down. This expression tells us what @dump
saw is a tuple of 2 elements (hence head
is :tuple
and args
is Array{Any}((2,))
). The two arguments are another expression (which is the way function calls are represented) and a Symbol
(in this case, :mystruct
).
Now for the second expression:
julia> Meta.@dump f(a,b*value(mystruct))
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol f
2: Symbol a
3: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol *
2: Symbol b
3: Expr
head: Symbol call
args: Array{Any}((2,))
1: Symbol value
2: Symbol mystruct
This is what we want to get out of the macro. Notice how this expression immediately starts with :call
! It’s not wrapped in a :tuple
expression anymore. We can also see that the third argument to the call (which used to be just :b
above) is now another expression calling *
, which itself has another :call
embedded in it, this time to the function value
. The whole inner call with :value
and :mystruct
corresponds to value(mystruct)
. Now, how do we get from the first form to the second form? We simply build the expression in our macro and return it:
macro myMacro(ex, mystruct)
#do error handling here, e.g. if `ex` is not a function call!
local tmp = ex.args[3] # get the third argument, which is `b`
local valueExpr = :(value($mystruct))
local multiplyExpr = Expr(:call, :*, tmp, valueExpr)
ex.args[3] = multiplyExpr
return ex
end
Using this, we get:
julia> struct MyStruct
end
julia> f(a, b) = a * b
f (generic function with 1 method)
julia> mystruct = MyStruct()
MyStruct()
julia> value(mystruct::MyStruct) = 2
value (generic function with 1 method)
julia> macro myMacro(ex, mystruct)
#do error handling here, e.g. if `ex` is not a function call!
local tmp = ex.args[3] # get the third argument, which is `b`
local valueExpr = :(value($mystruct))
local multiplyExpr = Expr(:call, :*, tmp, valueExpr)
ex.args[3] = multiplyExpr
return ex
end
@myMacro (macro with 1 method)
julia> a = 3 # define our variables
3
julia> b = 4 # define our variables
4
julia> @macroexpand @myMacro(f(a,b), mystruct)
:(Main.f(Main.a, Main.b * Main.value(Main.mystruct))) # sanity check, this looks like what we care about
julia> @myMacro(f(a,b), mystruct)
24 # 3*4*2 = 24, seems to have worked!
I hope this helps!
On a side note, the way this macro is written is very crude and should use some sanity checking and escaping logic, to make sure the correct variables are referenced in all contexts This is called “Macro Hygiene”, the documentation as a great chapter about it here.
If we don’t practice hygiene, the macro won’t work correctly in other scopes:
julia> function x()
a = 0
b = 0
mystruct = MyStruct()
@myMacro(f(a,b), mystruct)
end
x (generic function with 1 method)
julia> x()
24 # we defined `a` and `b` to be `0` in `x()`, so we would expect to get `0` out as well! Why don't we?
# Let's find out why by checking with `@macroexpand`
julia> @macroexpand function x()
a = 0
b = 0
mystruct = MyStruct()
@myMacro(f(a,b), mystruct)
end
:(function x()
#= REPL[25]:1 =#
#= REPL[25]:2 =#
a = 0
#= REPL[25]:3 =#
b = 0
#= REPL[25]:4 =#
mystruct = MyStruct()
#= REPL[25]:5 =#
Main.f(Main.a, Main.b * Main.value(Main.mystruct)) # Here's the culprit! Everything is referencing `Main`, which it clearly shouldn't here...
end)