Metaprogramming
The strongest legacy of Lisp in the Julia language is its metaprogramming support. Like Lisp, Julia is homoiconic: it represents its own code as a data structure of the language itself. Since code is represented by objects that can be created and manipulated from within the language, it is possible for a program to transform and generate its own code. This allows sophisticated code generation without extra build steps, and also allows true Lisp-style macros, as compared to preprocessor “macro” systems, like that of C and C++, that perform superficial textual manipulation as a separate pass before any real parsing or interpretation occurs. Another aspect of metaprogramming is reflection: the ability of a running program to dynamically discover properties of itself. Reflection emerges naturally from the fact that all data types and code are represented by normal Julia data structures, so the structure of the program and its types can be explored programmatically just like any other data.Expressions and Eval
Julia code is represented as a syntax tree built out of Julia data structures of type Expr. This makes it easy to construct and manipulate Julia code from within Julia, without generating or parsing source text. Here is the definition of the Expr type:type Expr
head::Symbol
args::Array{Any,1}
typ
end
There is special syntax for “quoting” code (analogous to quoting strings) that makes it easy to create expression objects without explicitly constructing Expr objects. There are two forms: a short form for inline expressions using : followed by a single expression, and a long form for blocks of code, enclosed in quote ... end. Here is an example of the short form used to quote an arithmetic expression:
julia> ex = :(a+b*c+1)
+(a,*(b,c),1)
julia> typeof(ex)
Expr
julia> ex.head
call
julia> typeof(ans)
Symbol
julia> ex.args
{+,a,*(b,c),1}
julia> typeof(ex.args[1])
Symbol
julia> typeof(ex.args[2])
Symbol
julia> typeof(ex.args[3])
Expr
julia> typeof(ex.args[4])
Int64
julia> quote
x = 1
y = 2
x + y
end
begin
x = 1
y = 2
+(x,y)
end
julia> :foo
foo
julia> typeof(ans)
Symbol
Eval and Interpolation
Given an expression object, one can cause Julia to evaluate (execute) it at the top level scope — i.e. in effect like loading from a file or typing at the interactive prompt — using the eval function:julia> :(1 + 2)
+(1,2)
julia> eval(ans)
3
julia> ex = :(a + b)
+(a,b)
julia> eval(ex)
a not defined
julia> a = 1; b = 2;
julia> eval(ex)
3
julia> ex = :(x = 1)
x = 1
julia> x
x not defined
julia> eval(ex)
1
julia> x
1
Since expressions are just Expr objects which can be constructed programmatically and then evaluated, one can, from within Julia code, dynamically generate arbitrary code which can then be run using eval. Here is a simple example:
julia> a = 1;
julia> ex = Expr(:call, {:+,a,:b}, Any)
+(1,b)
julia> a = 0; b = 2;
julia> eval(ex)
3
- The value of the variable a at expression construction time is used as an immediate value in the expression. Thus, the value of a when the expression is evaluated no longer matters: the value in the expression is already 1, independent of whatever the value of a might be.
- On the other hand, the symbol :b is used in the expression construction, so the value of the variable b at that time is irrelevant — :b is just a symbol and the variable b need not even be defined. At expression evaluation time, however, the value of the symbol :b is resolved by looking up the value of the variable b.
julia> a = 1;
1
julia> ex = :($a + b)
+(1,b)
Code Generation
When a significant amount of repetitive boilerplate code is required, it is common to generate it programmatically to avoid redundancy. In most languages, this requires an extra build step, and a separate program to generate the repetitive code. In Julia, expression interpolation and eval allow such code generation to take place in the normal course of program execution. For example, the following code defines a series of operators on three arguments in terms of their 2-argument forms:for op = (:+, :*, :&, :|, :$)
eval(quote
($op)(a,b,c) = ($op)(($op)(a,b),c)
end)
end
for op = (:+, :*, :&, :|, :$)
eval(:(($op)(a,b,c) = ($op)(($op)(a,b),c)))
end
for op = (:+, :*, :&, :|, :$)
@eval ($op)(a,b,c) = ($op)(($op)(a,b),c)
end
@eval begin
# multiple lines
end
julia> $a + b
not supported
Macros
Macros are the analogue of functions for expression generation at compile time: they allow the programmer to automatically generate expressions by transforming zero or more argument expressions into a single result expression, which then takes the place of the macro call in the final syntax tree. Macros are invoked with the following general syntax:@name expr1 expr2 ...
macro name(expr1, expr2, ...)
...
end
macro assert(ex)
:($ex ? nothing : error("Assertion failed: ", $string(ex)))
end
julia> @assert 1==1.0
julia> @assert 1==0
Assertion failed: 1==0
1==1.0 ? nothing : error("Assertion failed: ", "1==1.0")
1==0 ? nothing : error("Assertion failed: ", "1==0")
Hygiene
An issue that arises in more complex macros is that of hygiene. In short, one needs to ensure that variables introduced and used by macros do not accidentally clash with the variables used in code interpolated into those macros. To demonstrate the problem before providing the solution, let us consider writing a @time macro that takes an expression as its argument, records the time, evaluates the expression, records the time again, prints the difference between the before and after times, and then has the value of the expression as its final value. A naïve attempt to write this macro might look like this:macro time(ex)
quote
local t0 = time()
local val = $ex
local t1 = time()
println("elapsed time: ", t1-t0, " seconds")
val
end
end
julia> @time begin
local t = 0
for i = 1:10000000
t += i
end
t
end
elapsed time: 1.1377708911895752 seconds
50000005000000
julia> @time begin
local t0 = 0
for i = 1:10000000
t0 += i
end
t0
end
syntax error: local t0 declared twice
begin
local t0 = time()
local val = begin
local t0 = 0
for i = 1:100000000
t0 += i
end
t0
end
local t1 = time()
println("elapsed time: ", t1-t0, " seconds")
val
end
To address the macro hygiene problem, Julia provides the gensym function, which generates unique symbols that are guaranteed not to clash with any other symbols. Called with no arguments, gensym returns a single unique symbol:
julia> s = gensym()
#1007
julia> s1, s2 = gensym(2)
(#1009,#1010)
julia> s1
#1009
julia> s2
#1010
macro time(ex)
t0, val, t1 = gensym(3)
quote
local $t0 = time()
local $val = $ex
local $t1 = time()
println("elapsed time: ", $t1-$t0, " seconds")
$val
end
end
Non-Standard String Literals
Recall from Strings that string literals prefixed by an identifier are called non-standard string literals, and can have different semantics than un-prefixed string literals. For example:- E"$100\n" interprets escape sequences but does no string interpolation
- r"^\s*(?:#|$)" produces a regular expression object rather than a string
- b"DATA\xff\u2200" is a byte array literal for [68,65,84,65,255,226,136,128].
macro r_str(p)
Regex(p)
end
Regex("^\\s*(?:#|\$)")
for line = lines
m = match(r"^\s*(?:#|$)", line)
if m.match == nothing
# non-comment
else
# comment
end
end
re = Regex("^\\s*(?:#|\$)")
for line = lines
m = match(re, line)
if m.match == nothing
# non-comment
else
# comment
end
end
The mechanism for user-defined string literals is deeply, profoundly powerful. Not only are Julia’s non-standard literals implemented using it, but also the command literal syntax (`echo "Hello, $person"`) and regular string interpolation are implemented using it. These two powerful facilities are implemented with the following innocuous-looking pair of macros:
macro cmd(str)
:(cmd_gen($shell_parse(str)))
end
macro str(s)
interp_parse(s)
end
No comments:
Post a Comment
Thank you