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.)
The basic objects in this approach are unevaluated symbols:
:a # "the symbol a"
:a
typeof(:a)
Symbol
:a
refers to the symbol a
. We can evaluate it with the eval
function:
eval(:a)
a not defined while loading In[5], in expression starting on line 1
a
must be defined for this to work:
a = 3
3
eval(:a)
3
The eval
function takes an expression and evaluates it, that is, generates the corresponding code
Everything is a symbol:
:+, :sin
(:+,:sin)
typeof(:+)
Symbol
Symbols may be combined into expressions, which are the basic objects that represent pieces of Julia code:
ex = :(a + b) # the expression 'a+b'
:(a + b)
typeof(ex)
Expr
ex
:(a + b)
b = 7
eval(ex)
10
An expression is just a Julia object, so we can introspect (find out information about it):
names(ex)
3-element Array{Symbol,1}: :head :args :typ
# ex.<TAB>
ex.head
:call
ex.args
3-element Array{Any,1}: :+ :a :b
ex.typ
Any
More complicated expressions are represented as "abstract syntax trees" (ASTs), consisting of expressions nested inside expressions:
ex = :( sin(3a + 2b^2) )
:(sin(3a + 2 * b^2))
ex.args
2-element Array{Any,1}: :sin :(3a + 2 * b^2)
typeof(ex.args[2])
Expr
ex.args[2].args
3-element Array{Any,1}: :+ :(3a) :(2 * b^2)
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
:(begin # In[19], line 3: y = 3 # line 4: z = sin(y + 1) end)
y
y not defined while loading In[70], in expression starting on line 1
eval(ex2)
z
-0.7568024953079282
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)
(:block, (:line, 3, :In[19]), (:(=), :y, 3), :( # line 4:), (:(=), :z, (:call, :sin, (:call, :+, :y, 1))) )
Another way of seeing the structure is with dump
:
dump(ex2)
Expr head: Symbol block args: Array(Any,(4,)) 1: Expr head: Symbol line args: Array(Any,(2,)) 1: Int64 3 2: Symbol In[19] typ: Any 2: Expr head: Symbol = args: Array(Any,(2,)) 1: Symbol y 2: Int64 3 typ: Any 3: LineNumberNode line: Int64 4 4: Expr head: Symbol = args: Array(Any,(2,)) 1: Symbol z 2: Expr head: Symbol call args: Array(Any,(2,)) typ: Any typ: Any typ: Any
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 on the fly from within Julia. This is known as metaprogramming: programming a program.
The name macro is given to a kind of "super-function" that takes a piece of code as an argument, and returns an altered piece of code. A macro is thus a very different kind of object than a standard function. [Although it can be thought of as a function in the mathematical sense of the word.]
The Julia manual puts it like this:
macros map a tuple of argument expressions to a returned expression
Although metaprogramming is possible in many languages (including Python), Julia makes it particularly natural (although not exactly "easy"!)
Metaprogramming is useful in a variety of settings:
Macros are invoked using the @
sign, e.g.
@time sin(10)
elapsed time: 0.004374081 seconds (47856 bytes allocated)
-0.5440211108893698
A trivial example of defining a macro is the following, which duplicates whatever code it is passed. The $
sign is used to interpolate the value of the expression (similar to its usage for string interpolation):
macro duplicate(ex)
quote
$ex
$ex
end
end
@duplicate println(sin(10))
-0.5440211108893698 -0.5440211108893698
ex = :(@duplicate println(sin(10)))
:(@duplicate println(sin(10)))
eval(ex)
-0.5440211108893698 -0.5440211108893698
typeof(ex)
Expr
We can see what effect the macro actually has using macroexpand
:
macroexpand(ex)
:(begin # In[23], line 3: println(sin(10)) # line 4: println(sin(10)) end)
macroexpand(:(@time sin(10)))
:(begin # util.jl, line 50: local #261#b0 = Base.gc_bytes() # line 51: local #262#t0 = Base.time_ns() # line 52: local #263#g0 = Base.gc_time_ns() # line 53: local #264#val = sin(10) # line 54: local #265#g1 = Base.gc_time_ns() # line 55: local #266#t1 = Base.time_ns() # line 56: local #267#b1 = Base.gc_bytes() # line 57: Base.time_print(Base.-(#266#t1,#262#t0),Base.-(#267#b1,#261#b0),Base.-(#265#g1,#263#g0)) # line 58: #264#val end)
Exercise: Define a macro @until
that does an until
loop.
macro until(expr1, expr2)
quote
#:(
while !($expr1) # code interpolation
$expr2
end
#)
end
end
i = 0
@until i==10 begin
println(i)
i += 1
end
0 1 2 3 4 5 6 7 8 9
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
horner
horner not defined while loading In[35], in expression starting on line 1
# 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)
47
[Even though the Horner macro is not exported in Base
, we can access it as @Base.Math.horner
]
To see what the macro does to this call, we again use macroexpand
:
macroexpand(:(@horner(x, 2, 3, 4)))
:(begin #269#t = x 2 + #269#t * (3 + #269#t * 4) end)
macroexpand(:(@Base.Math.horner(x, 2, 3, 4)))
:(begin #292#t = x Base.Math.+(2,Base.Math.*(#292#t,Base.Math.+(3,Base.Math.*(#292#t,4)))) end)
x = 3.5
@printf("%.5f", x)
3.50000
@printf
@printf: called with zero arguments while loading In[2], in expression starting on line 1
ex = :(@time sin(10))
:(@time sin(10))
Meta.show_sexpr(ex)
(:macrocall, :@time, (:call, :sin, 10))
xdump(ex)
Expr head: Symbol macrocall args: Array(Any,(2,)) 1: Symbol @time 2: Expr head: Symbol call args: Array(Any,(2,)) 1: Symbol sin 2: Int64 10 typ: Any::DataType <: Any typ: Any::DataType <: Any
macroexpand(ex)
:(begin # util.jl, line 50: local #254#b0 = Base.gc_bytes() # line 51: local #255#t0 = Base.time_ns() # line 52: local #256#g0 = Base.gc_time_ns() # line 53: local #257#val = sin(10) # line 54: local #258#g1 = Base.gc_time_ns() # line 55: local #259#t1 = Base.time_ns() # line 56: local #260#b1 = Base.gc_bytes() # line 57: Base.time_print(Base.-(#259#t1,#255#t0),Base.-(#260#b1,#254#b0),Base.-(#258#g1,#256#g0)) # line 58: #257#val end)
@time sin(10)
elapsed time: 0.006097425 seconds (47856 bytes allocated)
-0.5440211108893698
dump(ex)
Expr head: Symbol macrocall args: Array(Any,(2,)) 1: Symbol @time 2: Expr head: Symbol call args: Array(Any,(2,)) 1: Symbol sin 2: Int64 10 typ: Any typ: Any
xdump(ex)
Expr head: Symbol macrocall args: Array(Any,(2,)) 1: Symbol @time 2: Expr head: Symbol call args: Array(Any,(2,)) 1: Symbol sin 2: Int64 10 typ: Any::DataType <: Any typ: Any::DataType <: Any
type Vector2D{T <: Real}
x::T
y::T
end
methods(Vector2D)
v = Vector2D{Float64}(3, 4)
Vector2D{Float64}(3.0,4.0)
v = Vector2D(3., 4)
no method Vector2D{T<:Real}(Float64, Int64) while loading In[11], in expression starting on line 1
v = Vector2D(3., 4.)
Vector2D{Float64}(3.0,4.0)
# clear
# uses show(io::IO, v::DataType) by default
typeof(Vector2D)
DataType
super(Vector2D)
Any
methods(show)
Base.show(io::IO, v::Vector2D) = print(io, "[$(v.x), $(v.y)]")
show (generic function with 91 methods)
v
[3.0, 4.0]
Vector2D(3im, 4im)
no method Vector2D{T<:Real}(Complex{Int64}, Complex{Int64}) while loading In[19], in expression starting on line 1
code =
"""
function testinf(a, b)
y = a + b
return sin(y)
end
"""
ex = parse(code)
eval(ex)
testinf (generic function with 1 method)
function testinf(a, b)
y = a + b
return sin(y)
end
testinf (generic function with 1 method)
code_lowered(testinf,(Int, Int))
1-element Array{Any,1}: :($(Expr(:lambda, {:a,:b}, {{:y},{{:a,:Any,0},{:b,:Any,0},{:y,:Any,18}},{}}, :(begin # In[21], line 2: y = a + b # line 3: return sin(y) end))))
code_typed(testinf, (Int, Int))
1-element Array{Any,1}: :($(Expr(:lambda, {:a,:b}, {{:y,:_var1},{{:a,Int64,0},{:b,Int64,0},{:y,Int64,18},{:_var1,Float64,18}},{}}, :(begin # In[21], line 2: y = top(box)(Int64,top(add_int)(a::Int64,b::Int64))::Int64 # line 3: _var1 = GetfieldNode(Base.Math,:box,Any)(Float64,top(sitofp)(Float64,y::Int64))::Float64 return GetfieldNode(Base.Math,:nan_dom_err,Any)(top(ccall)($(Expr(:call1, :(top(tuple)), "sin", GetfieldNode(Base.Math,:libm,Any)))::(ASCIIString,ASCIIString),Float64,$(Expr(:call1, :(top(tuple)), :Float64))::(Type{Float64},),_var1::Float64,0)::Float64,_var1::Float64)::Float64 end::Float64))))
code_typed(testinf, (Float64, Float64))
1-element Array{Any,1}: :($(Expr(:lambda, {:a,:b}, {{:y},{{:a,Float64,0},{:b,Float64,0},{:y,Float64,18}},{}}, :(begin # In[21], line 2: y = top(box)(Float64,top(add_float)(a::Float64,b::Float64))::Float64 # line 3: return GetfieldNode(Base.Math,:nan_dom_err,Any)(top(ccall)($(Expr(:call1, :(top(tuple)), "sin", GetfieldNode(Base.Math,:libm,Any)))::(ASCIIString,ASCIIString),Float64,$(Expr(:call1, :(top(tuple)), :Float64))::(Type{Float64},),y::Float64,0)::Float64,y::Float64)::Float64 end::Float64))))
code_llvm(testinf, (Int, Int))
define double @"julia_testinf;19510"(i64, i64) { top: %2 = add i64 %1, %0, !dbg !2501 %3 = sitofp i64 %2 to double, !dbg !2502 %4 = call double inttoptr (i64 4514612992 to double (double)*)(double %3), !dbg !2502 %5 = fcmp ord double %4, 0.000000e+00, !dbg !2502 %6 = fcmp uno double %3, 0.000000e+00, !dbg !2502 %7 = or i1 %5, %6, !dbg !2502 br i1 %7, label %pass, label %fail, !dbg !2502 fail: ; preds = %top %8 = load %jl_value_t** @jl_domain_exception, align 8, !dbg !2502, !tbaa %jtbaa_const call void @jl_throw_with_superfluous_argument(%jl_value_t* %8, i32 3), !dbg !2502 unreachable, !dbg !2502 pass: ; preds = %top ret double %4, !dbg !2502 }
code_native(testinf, (Int, Int))
.section __TEXT,__text,regular,pure_instructions Filename: In[21] Source line: 2 push RBP mov RBP, RSP Source line: 2 sub RSP, 16 add RDI, RSI Source line: 3 vcvtsi2sd XMM0, XMM0, RDI vmovsd QWORD PTR [RBP - 8], XMM0 movabs RAX, 4514612992 call RAX vucomisd XMM0, XMM0 jp 6 add RSP, 16 pop RBP ret vmovsd XMM1, QWORD PTR [RBP - 8] vucomisd XMM1, XMM1 jp -21 movabs RAX, 4380089384 mov RDI, QWORD PTR [RAX] movabs RAX, 4367555552 mov ESI, 3 call RAX
names(testinf)
3-element Array{Symbol,1}: :fptr :env :code
testinf.fptr
Ptr{Void} @0x00000001044f2e50
testinf.code
access to undefined reference while loading In[38], in expression starting on line 1
?dump
INFO: Loading help data...
Base.dump(x) Show all user-visible structure of a value.
@until
exercise:¶macro until(ex1, ex2)
quote
while !($ex1)
$ex2
end
end
end
i = 1
@until i > 10 begin
println(i)
i += 1
end
1 2 3 4 5 6 7 8 9 10
i = 1
@until i > 10
begin
println(i)
i += 1
end
wrong number of arguments while loading In[11], in expression starting on line 4
:while
:while