Julia represents its own code as a data structure accessible from 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, that is to create powerful macros (the term "metaprogramming" refers to the possibility to write code that writes code that is then evaluated).
Note the difference from C or C++ macros. There, macros work performing textual manipulation and substitution before any actual parsing or interpretation occurs.
In Julia, macros work when the code has already been parsed and organised in a syntax tree, and hence the semantic is much richer and allows for much more powerful manipulations.
There are really many ways to create an expression:
The colon `:` prefix operator refers to an unevaluated expression. Such expression can be saved and then evaluated later using eval(myexpression)
:
expr = :(1+2) # save the `1+2` expression in the `expr` expression
eval(expr) # here the expression is evaluated and the code returns 3
Note that $ interpolation (like for strings) is supported:
a = 1
expr = :($a+2) # expr is now :(1+2)
An alternative of the :([...])
operator is to use the quote [...] end
block.
Or also, starting from a string (that is, the original representation of source code for Julia):
expr = Meta.parse("1+2") # parses the string "1+2" and saves the `1+2` expression in the `expr` expression, same as expr = :(1+2)
eval(expr) # here the expression is evaluated and the code returns 3
The expression can be also directly constructed from the tree: expr = Expr(:call, :+, 1, 2)
is equivalent to expr = parse("1+2")
or expr = :(1+2)
.
But what is there inside an expression? Using fieldnames(typeof(expr))
or dump(expr)
we can find that expr
is an Expr
object made of two fields: :head
and :args
:
:head
defines the type of Expression, in this case:call
:args
is an array of elements that can be symbols, literal values or other expressions. In this case they are[:+, 1, 1]
The second meaning of the :
operator is to create symbols, and it is equivalent to the Symbol()
function that concatenates its arguments to form a symbol:
a = :foo10
is equal to a=Symbol("foo",10)
A useful example to highlight what a symbol is:
a = 2;
ex = Expr(:call, :*, a, :b) # ex is equal to :(2 * b). Note that b doesn't even need to be defined
a = 0; b = 2; # no matter what now happens to a, as a is evaluated at the moment of creating the expression and the expression stores its value, without any more reference to the variable
eval(ex) # returns 4, not 0
- To convert a string to symbol:
Symbol("mystring")
- To convert a Symbol to string:
String(mysymbol)
The possibility to represent code into expressions is at the heart of the usage of macros. Macros in Julia take one or more input expressions and return a modified expressions (at parse time). This contrast with normal functions that, at runtime, take the input values (arguments) and return a computed value.
Macro definition
macro unless(test_expr, branch_expr)
quote
if !$test_expr
$branch_expr
end
end
end
Macro call
array = [1, 2, 'b']
@unless 3 in array println("array does not contain 3") # here test_expr is "3 in array" and branch_expr is "println("array does not contain 3")"
Like for strings, the $
interpolation operator will substitute the variable with its content, in this context the expression. So the "expanded" macro will look in this case as:
if !(3 in array)
println("array does not contain 3")
end
Attention that the macro doesn't create a new scope, and variables declared or assigned within the macro may collide with variables in the scope of where the macro is actually called.
You can review the content of this section in this notebook.
While an updated, expanded and revised version of this chapter is available in "Chapter 6 - Metaprogramming and Macros" of Antonello Lobianco (2019), "Julia Quick Syntax Reference", Apress, this tutorial remains in active development.