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:
In [3]:
:a # "the symbol a"
Out[3]:
In [4]:
typeof(:a)
Out[4]:
:a
refers to the symbol a
. We can evaluate it with the eval
function:
In [5]:
eval(:a)
a
must be defined for this to work:
In [6]:
a = 3
Out[6]:
In [7]:
eval(:a)
Out[7]:
The eval
function takes an expression and evaluates it, that is, generates the corresponding code
Everything is a symbol:
In [8]:
:+, :sin
Out[8]:
In [9]:
typeof(:+)
Out[9]:
Symbols may be combined into expressions, which are the basic objects that represent pieces of Julia code:
In [10]:
ex = :(a + b) # the expression 'a+b'
Out[10]:
In [11]:
typeof(ex)
Out[11]:
In [12]:
ex
Out[12]:
In [13]:
b = 7
eval(ex)
Out[13]:
An expression is just a Julia object, so we can introspect (find out information about it):
In [60]:
names(ex)
Out[60]:
In [61]:
# ex.<TAB>
In [14]:
ex.head
Out[14]:
In [63]:
ex.args
Out[63]:
In [64]:
ex.typ
Out[64]:
More complicated expressions are represented as "abstract syntax trees" (ASTs), consisting of expressions nested inside expressions:
In [15]:
ex = :( sin(3a + 2b^2) )
Out[15]:
In [16]:
ex.args
Out[16]:
In [17]:
typeof(ex.args[2])
Out[17]:
In [18]:
ex.args[2].args
Out[18]:
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 :( ... )
In [19]:
ex2 =
quote
y = 3
z = sin(y+1)
end
Out[19]:
In [70]:
y
In [71]:
eval(ex2)
z
Out[71]:
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
:
In [20]:
Meta.show_sexpr(ex2)
Another way of seeing the structure is with dump
:
In [21]:
dump(ex2)
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.
In [22]:
@time sin(10)
Out[22]:
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):
In [23]:
macro duplicate(ex)
quote
$ex
$ex
end
end
In [24]:
@duplicate println(sin(10))
In [25]:
ex = :(@duplicate println(sin(10)))
Out[25]:
In [77]:
eval(ex)
In [78]:
typeof(ex)
Out[78]:
We can see what effect the macro actually has using macroexpand
:
In [26]:
macroexpand(ex)
Out[26]:
In [27]:
macroexpand(:(@time sin(10)))
Out[27]:
Exercise: Define a macro @until
that does an until
loop.
In [30]:
macro until(expr1, expr2)
quote
#:(
while !($expr1) # code interpolation
$expr2
end
#)
end
end
In [34]:
i = 0
@until i==10 begin
println(i)
i += 1
end
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
In [35]:
horner
In [37]:
# 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
In [38]:
x = 3
@horner(x, 2, 3, 4)
Out[38]:
[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
:
In [39]:
macroexpand(:(@horner(x, 2, 3, 4)))
Out[39]:
In [88]:
macroexpand(:(@Base.Math.horner(x, 2, 3, 4)))
Out[88]:
In [101]:
x = 3.5
@printf("%.5f", x)
In [2]:
@printf
In [102]:
ex = :(@time sin(10))
Out[102]:
In [103]:
Meta.show_sexpr(ex)
In [5]:
xdump(ex)
In [6]:
macroexpand(ex)
Out[6]:
In [7]:
@time sin(10)
Out[7]:
In [8]:
dump(ex)
In [9]:
xdump(ex)
In [8]:
type Vector2D{T <: Real}
x::T
y::T
end
In [9]:
methods(Vector2D)
Out[9]:
In [10]:
v = Vector2D{Float64}(3, 4)
Out[10]:
In [11]:
v = Vector2D(3., 4)
In [18]:
v = Vector2D(3., 4.)
Out[18]:
In [2]:
# clear
In [4]:
# uses show(io::IO, v::DataType) by default
In [15]:
typeof(Vector2D)
Out[15]:
In [13]:
super(Vector2D)
Out[13]:
In [3]:
methods(show)
Out[3]:
In [20]:
Base.show(io::IO, v::Vector2D) = print(io, "[$(v.x), $(v.y)]")
Out[20]:
In [18]:
v
Out[18]:
In [19]:
Vector2D(3im, 4im)
In [29]:
code =
"""
function testinf(a, b)
y = a + b
return sin(y)
end
"""
ex = parse(code)
eval(ex)
Out[29]:
In [21]:
function testinf(a, b)
y = a + b
return sin(y)
end
Out[21]:
In [24]:
code_lowered(testinf,(Int, Int))
Out[24]:
In [22]:
code_typed(testinf, (Int, Int))
Out[22]:
In [23]:
code_typed(testinf, (Float64, Float64))
Out[23]:
In [25]:
code_llvm(testinf, (Int, Int))
In [26]:
code_native(testinf, (Int, Int))
In [32]:
names(testinf)
Out[32]:
In [34]:
testinf.fptr
Out[34]:
In [38]:
testinf.code
In [11]:
?dump
In [2]:
macro until(ex1, ex2)
quote
while !($ex1)
$ex2
end
end
end
In [10]:
i = 1
@until i > 10 begin
println(i)
i += 1
end
In [11]:
i = 1
@until i > 10
begin
println(i)
i += 1
end
In [13]:
:while
Out[13]:
In [ ]: