Julia Metaprogramming
Julia represents its own code as data structures in the language, allowing us to write programs that manipulate programs.
Metaprogramming can also be simply understood as writing code that generates code.
> **Metaprogramming** is the writing of computer programs that write or manipulate other programs (or themselves) as their data, or that do part of the work at compile time that would otherwise be done at runtime. In many cases, this allows programmers to be more productive than they could be by writing code manually, or gives programs greater flexibility to handle new situations without recompilation.
>
> The language used to write metaprograms is called the **meta language**. The language of the programs being manipulated is called the **target language**. A programming language's ability to be its own meta language is known as **reflection** or **reflexivity**.
>
> -- Wikipedia
### Julia Source Code Execution Phases
**1. Parse raw Julia code**: Julia's parser first parses the string to obtain an Abstract Syntax Tree (AST), which is a structure that contains all code in an easy-to-manipulate format.
**2. Execute parsed Julia code**: In this stage, the parsed Julia code is executed.
When we enter code in an interactive programming environment (REPL) and press Enter, both of these phases are executed.
Using metaprogramming tools, we can access the Julia code between these two phases, i.e., after source code parsing but before execution.
## Program Representation
Julia provides a Meta class, where Meta.parse(str) can be used to parse a string, and typeof(e1) returns Expr:
## Example
julia> prog = "1 + 1"
"1 + 1"
julia> ex1 = Meta.parse(prog)
:(1 + 1)
julia> typeof(ex1)
Expr
Returns **:(1 + 1)**, this returned value consists of a colon followed by the expression, and typeof(ex1) returns Expr.
An Expr object contains two parts (ex1 has head and args properties):
A symbol object that identifies the expression type.
## Example
julia> ex1.head
:call
The other is the arguments of the expression, which can be symbols, other expressions, or literals:
## Example
julia> ex1.args
3-element Vector{Any}:
:+
1
1
Expressions can also be constructed directly using Expr:
## Example
julia> ex2 = Expr(:call, :+, 1, 1)
:(1 + 1)
The two expressions constructed above: one through parsing and one through direct construction, they are equivalent:
## Example
julia> ex1 == ex2
true
Expr objects can also be nested:
## Example
julia> ex3 = Meta.parse("(4 + 4) / 2")
:((4 + 4) / 2)
We can also use Meta.show_sexpr to view expressions. The following example shows nested Expr:
## Example
julia> Meta.show_sexpr(ex3)
(:call, :/, (:call, :+, 4, 4), 2)
### Symbols
We can store an unevaluated but parsed expression using the colon **:** prefix operator.
## Example
julia> ABC = 100
100
julia> :ABC
:ABC
Quote the entire expression below:
## Example
julia> :(100-50)
:(100 - 50)
Quote an arithmetic expression:
## Example
julia> ex = :(a+b*c+1)
:(a + b * c + 1)
julia> typeof(ex)
Expr
Note that equivalent expressions can also be constructed using Meta.parse or directly using Expr:
## Example
julia>:(a + b*c + 1) ==
Meta.parse("a + b*c + 1") ==
Expr(:call, :+, :a, Expr(:call, :*, :b, :c), 1)
true
Multiple expressions can also be contained in a code block using **quote ... end**.
## Example
julia> ex = quote
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
julia> ex1 = :(1 + 2)
:(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 represents an abstract view of the syntax structure of source code.
It presents the syntax structure of a programming language in a tree form, where each node in the tree represents a structure in the source code.
We can view the hierarchical structure of an expression using the dump() function:
## Example
julia> dump(:(1* cos(pi/2)))
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
YouTip