A "meta" program is a program that manipulates programs.
Most common use refers to a program that generates another program.
In the last class we saw how Julia can generate specialized code for you.
Sometimes that's not enough, and you need to write a program to explicitly generate the code needed for a specialized problem.
Julia allows us to talk in a "meta" way ("one level up"), about Julia code, that is to "treat code as data" and manipulate it as just another object in Julia. (This is very similar to Lisp.)
One of the most basic objects in this approach are unevaluated symbols:
:a # "the symbol a"
typeof(:a)
:a
refers to the symbol a
. We can evaluate it with the eval
function:
eval(:a)
a
must be defined for this to work:
a = 3
eval(:a)
The eval
function takes an expression and evaluates it, that is, generates the corresponding code
Everything is a symbol:
:+, :sin
typeof(:+)
Symbols may be combined into expressions, which are the basic objects that represent pieces of Julia code:
ex = :(a + b) # the expression 'a + b'
typeof(ex)
b = 7
eval(ex)
An expression is just a Julia object, so we can introspect (find out information about it):
dump(ex)
dump(:(x = 3))
# ex.<TAB>
ex.head
ex.args
ex.typ
The job of Julia's parser is to convert a sequence of characters into these Expr
objects:
parse("a + b")
More complicated expressions are represented as "abstract syntax trees" (ASTs), consisting of expressions nested inside expressions:
ex = :( sin(3a + 2b^2) )
ex.args
typeof(ex.args[2])
ex.args[2].args
ex.args[1] = :cos
ex
blk = quote
println("Hello")
end
eval(blk)
push!(blk.args, :(println("AFTER")))
blk
eval(blk)
unshift!(blk.args, :(println("BEFORE")))
eval(blk)
Expressions can be arbitrary Julia code that when evaluated will have side effects. For longer blocks of code, quote...end
may be used instead of :( ... )
ex2 =
quote
y = 3
z = sin(y+1)
end
eval(ex2)
y
eval(ex2)
z
The full form of the abstract syntax tree in a style similar to a Lisp s-expression can be obtained using functions from the Meta
module in Base
:
Meta.show_sexpr(ex2)
Another way of seeing the structure is with dump
:
dump(ex2)
You will sometimes see @eval expr
, which is shorthand for:
eval(quote
expr
end)
allowing items to be interpolated immediately.
fnames = [ Symbol("func$i") for i=1:2]
for i = 1:length(fnames)
@eval ($(fnames[i]))(x) = $i
end
func1(0)
func2(0)
With the ability to think of code in terms of a data structure in the Julia language, we can now manipulate those data structures, allowing us to create Julia code from within Julia.
Macros provide a particular use pattern of metaprogramming: replacing one expression with another, in-place, right after parsing.
The Julia manual puts it like this:
macros map a tuple of argument expressions to a returned expression
Macros are useful in several cases:
Macros are invoked using the @
sign, e.g.
@time sin(10)
A trivial example of defining a macro is the following, which runs whatever code it is passed two times. The $
sign is used to interpolate the value of the expression (similar to its usage for string interpolation):
macro twice(ex)
quote
$ex
$ex
end
end
x = 0
@twice println(x += 1)
ex = :(@twice println(x += 1))
eval(ex)
typeof(ex)
We can see what effect the macro actually has using macroexpand
:
macroexpand(:(@twice println(x += 1)))
macroexpand(:(@time sin(10)))
macro mytime(ex)
quote
t0 = time()
val = $ex
t1 = time()
println("$(t1-t0) seconds elapsed")
val
end
end
@mytime (sleep(1); "done")
Exercise: Define a macro @until
that does an until
loop.
macro set_x(val)
:(x = $val)
end
@set_x 10
x
macro set_x(val)
:($(esc(:x)) = $val)
end
@set_x 10
x
Recall the hand-specialized method for s == 1
:
function circularshift1!(X::AbstractVector)
n = length(X)
temp = X[n]
copy!(X, 2, X, 1, n-1)
X[1] = temp
return X
end
We can use a macro to generate extra temp variables for us.
macro load_n_temps(n::Int, arr, len)
blk = Expr(:block)
for i = 1:n
var = esc(Symbol("_temp$i"))
push!(blk.args, :($var = ($arr)[$len - $n + $i]))
end
blk
end
macro store_n_temps(n::Int, arr)
blk = Expr(:block)
for i = 1:n
var = esc(Symbol("_temp$i"))
push!(blk.args, :(($arr)[$i] = $var))
end
blk
end
function circularshift4!(X::AbstractVector)
n = length(X)
@load_n_temps 4 X n
copy!(X, 2, X, 1, n-1)
@store_n_temps 4 X
return X
end
circularshift4!(collect(1:10))
There are many interesting examples of macros in Base
. One that is accessible is Horner's method for evaluating a polynomial:
may be evaluated efficiently as
$$p(x) = a_0 + x(a_1 + \cdots x(a_{n-2} + \cdots + x(a_{n-1} + x a_n) \cdots ) ) $$with only $n$ multiplications.
The obvious way to do this is with a for
loop. But if we know the polynomial at compile time, this loop may be unrolled using metaprogramming. This is implemented in the Math
module in math.jl
in Base
, so the name of the macro which is not exported is @Base.Math.horner
, so in the current namespace, horner
should be undefined:
horner
# copied from base/math.jl
macro horner(x, p...)
ex = esc(p[end])
for i = length(p)-1:-1:1
ex = :( $(esc(p[i])) + t * $ex )
end
Expr(:block, :(t = $(esc(x))), ex)
end
This is called as follows: to evaluate the polynomial $p(x) = 2 + 3x + 4x^2$ at $x=3$, we do
x = 3
@horner(x, 2, 3, 4, 5)
To see what the macro does to this call, we again use macroexpand
:
macroexpand(:(@horner(x, 2, 3, 4, 5, 6, 7, 8, 9, 10)))
macroexpand(:(@Base.Math.horner(x, 2, 3, 4, 5)))
?muladd
?fma
f(x) = Base.Math.@horner(x, 1.2, 2.3, 3.4, 4.5)
@code_llvm f(0.1)
@code_native f(0.1)