Assuming that you really need to generate code, definitely, definitely option 1, not option 2. Julia’s metaprogramming facilities are excellent, and you would lose all of that elegance and flexibility by trying to manually generate source code from strings. Essentially, in order to safely generate valid Julia code as strings, you would have to build up your own data structures and functions for representing that code. But Julia already provides all of that, so why not use it?
All you need to do to satisfy option 1 is:
- Produce the relevant struct and function definitions by building up Exprs
- Pass those definitions to
eval
- That’s it
For example, let’s say we want to generate a struct which has fields whose names are stored in a text file:
julia> read_text_file() = ["a", "b", "c"] # just a dummy implementation for testing
read_text_file (generic function with 1 method)
julia> expr = quote
struct Foo
$(Symbol.(read_text_file())...)
end
end
quote
#= REPL[2]:2 =#
struct Foo
#= REPL[2]:3 =#
a
b
c
end
end
julia> eval(expr)
julia> f = Foo(1,2,3)
Foo(1, 2, 3)
Easy!
The way I usually approach metaprogramming tasks like this is to first try to manually generate the kind of output I want and then use the dump()
command to inspect that expression:
julia> target = quote
struct Foo
a
b
c
end
end
quote
#= REPL[2]:2 =#
struct Foo
#= REPL[2]:3 =#
a
#= REPL[2]:4 =#
b
#= REPL[2]:5 =#
c
end
end
julia> dump(target)
Expr
head: Symbol block
args: Array{Any}((2,))
1: LineNumberNode
line: Int64 2
file: Symbol REPL[2]
2: Expr
head: Symbol struct
args: Array{Any}((3,))
1: Bool false
2: Symbol Foo
3: Expr
head: Symbol block
args: Array{Any}((6,))
1: LineNumberNode
line: Int64 3
file: Symbol REPL[2]
2: Symbol a
3: LineNumberNode
line: Int64 4
file: Symbol REPL[2]
4: Symbol b
5: LineNumberNode
line: Int64 5
file: Symbol REPL[2]
6: Symbol c
as long as you can produce an Expr
that matches your target, you can be reasonably sure that it will work.
One of the best parts of Julia metaprogramming is that you can let the parser do some of the work of generating valid Expr
s for you. For example, let’s generate an empty struct expression:
julia> expr = quote
struct Foo
end
end
quote
#= REPL[4]:2 =#
struct Foo
#= REPL[4]:3 =#
end
end
julia> dump(expr)
Expr
head: Symbol block
args: Array{Any}((2,))
1: LineNumberNode
line: Int64 2
file: Symbol REPL[4]
2: Expr
head: Symbol struct
args: Array{Any}((3,))
1: Bool false
2: Symbol Foo
3: Expr
head: Symbol block
args: Array{Any}((1,))
1: LineNumberNode
line: Int64 3
file: Symbol REPL[4]
The quote
command created a block
expression, and inside that block
is our struct
expression. To add a field to the struct, we just need to modify the args
field of that inner expression:
julia> push!(expr.args[2].args[3].args, :a)
2-element Array{Any,1}:
:(#= REPL[4]:3 =#)
:a
julia> expr
quote
#= REPL[4]:2 =#
struct Foo
#= REPL[4]:3 =#
a
end
end
and now we can evaluate the result to actually define that struct:
julia> eval(expr)
julia> f = Foo(1)
Foo(1)
and at no point did we ever have to do any kind of string concatenation!