Julia Metaprogramming
Julia represents its own code as data structures in the language, so that we can write programs that manipulate programs.
Metaprogramming can also be simply understood as writing code that can generate code.
Metaprogramming refers to the writing of a certain class of computer programs that write or manipulate other programs (or themselves) as their data, or that accomplish at compile time part of the work that should have been done at runtime. In most cases, compared with writing all code by hand, programmers can achieve higher work efficiency, or give the program greater flexibility to handle new situations without recompiling.
The language used to write metaprograms is called the metalanguage. The language of the program being manipulated is called the "target language". The ability of a programming language to also be its own metalanguage is called "reflection" or "reflexivity".
-- Wikipedia
Julia Source Code Execution Stages
1. Parse the original Julia code: The Julia parser first parses the string to obtain an abstract syntax tree (AST), which is a structure that contains all the code in an easy-to-manipulate format.
2. Execute the parsed Julia code:: In this stage, the parsed Julia code is executed.
When we enter code in the interactive programming environment (REPL) and press Enter, the above two stages are executed.
With metaprogramming tools, we can access the Julia code between these two stages, that is, after the source code is parsed but before it is executed.
Program Representation
Julia provides a Meta module, in which Meta.parse(str) can be used to parse a string, and typeof(e1) returns Expr:
Example
"1 + 1"
julia> ex1 = Meta.parse(prog)
:(1 + 1)
julia> typeof(ex1)
Expr
Returns:(1 + 1)The returned value consists of a colon and the following expression; typeof(ex1) returns Expr.
The Expr object contains two parts (ex1 contains the head and args attributes):
One is a symbol object that identifies the type of the expression.
Example
:call
The other is the arguments of the expression, which may be symbols, other expressions, or literals:
Example
3-element Vector{Any}:
:+
1
1
Expressions can also be constructed directly with Expr:
Example
:(1 + 1)
The two expressions constructed above, one by parsing and one by direct construction, are equivalent:
Example
true
Expr objects can also be nested:
Example
:((4 + 4) / 2)
We can also use Meta.show_sexpr to view expressions. The following example demonstrates nested Expr:
Example
(:call, :/, (:call, :+, 4, 4), 2)
Symbols
We can use the colon:prefix operator to store an unevaluated but parsed expression.
Example
100
julia> :ABC
:ABC
Quote the entire expression below:
Example
:(100 - 50)
Quote an arithmetic expression:
Example
:(a + b * c + 1)
julia> typeof(ex)
Expr
Note that equivalent expressions can also be constructed using Meta.parse or directly with Expr:
Example
Meta.parse("a + b*c + 1") ==
Expr(:call, :+, :a, Expr(:call, :*, :b, :c), 1)
true
Quoting multiple expressions can alsoquote ... endinclude a code block in it.
Example
x = 1
y = 2
x + y
end
quote
#= none:2 =#
x = 1
#= none:3 =#
y = 2
#= none:4 =#
x + y
end
julia> typeof(ex)
Expr
Executing Expressions
After an expression is parsed, we can use the eval() function to execute it:
Example
:(1 + 2)
julia> eval(ex1)
3
julia> ex = :(a + b)
:(a + b)
julia> eval(ex)
ERROR: UndefVarError: b not defined
[...]
julia> a = 1; b = 2;
julia> eval(ex)
3
Abstract Syntax Tree (AST)
An abstract syntax tree (AST) is a structure that is an abstract representation of the grammatical structure of source code.
It represents the syntactic structure of a programming language in tree form; each node on the tree represents a structure in the source code.
We can view the hierarchical structure of an expression with the dump() function:
Example
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol *
2: Int64 1
3: Expr
head: Symbol call
args: Array{Any}((2,))
1: Symbol cos
2: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol /
2: Symbol pi
3: Int64 2
Interpolation
Although constructing Expr objects directly with value parameters is powerful, compared with Julia syntax, the Expr constructor may feel tedious. As an alternative, Julia allows literals or expressions to be inserted into quoted expressions. Expression interpolation is indicated by the prefix $.
In this example, the value of variable a is interpolated:
Example
julia> ex = :($a + b)
:(1 + b)
Interpolating into an unquoted expression is not supported, which causes a compile-time error:
julia> $a + b ERROR: syntax: "$" expression outside quote
In this example, the tuple (1,2,3) is inserted as an expression into the conditional test:
julia> ex = :(a in $:((1,2,3)) ) :(a in (1, 2, 3))
The use of $ in expression interpolation is deliberately reminiscent of string interpolation and command interpolation. Expression interpolation makes the programmatic construction of complex Julia expressions convenient and readable.
Macro
Macros provide a mechanism for including generated code in the final body of a program. A macro maps a set of arguments to a returned expression, and the generated expression is compiled directly without requiring a runtime eval call. Macro arguments may include expressions, literals, and symbols.
This is a very simple macro:
Example
return :( println("Hello, world!") )
end
@sayhello (macro with 1 method)
In Julia's syntax, macros have a special character @ (at-sign), immediately followed by a unique macro name declared using the form macro NAME ... end. In this example, the compiler will replace all @sayhello with:
:( println("Hello, world!") )
When @sayhello is entered in the REPL, the interpreter executes it immediately, so we only see the evaluated result:
julia> @sayhello() Hello, world!
Now, consider a slightly more complex macro:
Example
return :( println("Hello, ", $name) )
end
@sayhello (macro with 1 method)
This macro accepts an argument name. When @sayhello is encountered, the quoted expression is expanded and the value of the argument is inserted into the final expression:
Example
Hello, human
We can use the macroexpand function to view the returned quoted expression:
Example
:(Main.println("Hello, ", "human"))
julia> typeof(ex)
Expr
We can see that the literal "human" has been inserted into the expression.
There is also a macro @macroexpand, which may be more convenient than the macroexpand function:
Example
:(println("Hello, ", "human"))