Skip to content

Julia Metaprogramming

Metaprogramming is writing code that manipulates code itself. Julia's metaprogramming capabilities are very powerful, supporting expression manipulation, macros, and code generation.

Expressions and Symbols

Symbols

julia
# Create symbol
s = :hello
println(s)          # hello
println(typeof(s))  # Symbol

# Symbol and string conversion
s = Symbol("world")
str = String(:hello)

# Symbol comparison
println(:apple == :apple)  # true
println(:apple == :banana) # false

Expressions (Expr)

julia
# Use :() to quote expression
expr = :(1 + 2)
println(expr)           # :(1 + 2)
println(typeof(expr))   # Expr

# View expression structure
println(expr.head)      # :call
println(expr.args)      # Any[:+, 1, 2]

# Use Meta.parse to parse string
expr = Meta.parse("x + y * z")
println(expr)

# Using quote block
expr = quote
    x = 1
    y = 2
    x + y
end
println(expr)

Expression Structure

julia
expr = :(a + b * c)

# Use dump to view full structure
dump(expr)
# Expr
#   head: Symbol call
#   args: Array{Any}((3,))
#     1: Symbol +
#     2: Symbol a
#     3: Expr
#       head: Symbol call
#       args: Array{Any}((3,))
#         1: Symbol *
#         2: Symbol b
#         3: Symbol c

Expression Manipulation

Building Expressions

julia
# Manually build expression
expr = Expr(:call, :+, 1, 2)
println(expr)        # :(1 + 2)
println(eval(expr))  # 3

# Build assignment expression
expr = Expr(:(=), :x, 10)
println(expr)  # :(x = 10)

# Build function call
expr = Expr(:call, :println, "Hello, World!")
eval(expr)  # Hello, World!

Modifying Expressions

julia
expr = :(x + y)

# Modify operator
expr.args[1] = :*
println(expr)  # :(x * y)

# Modify operands
expr.args[2] = :a
expr.args[3] = :b
println(expr)  # :(a * b)

Expression Interpolation

julia
# Use $ for interpolation
x = 10
expr = :($x + 20)
println(expr)  # :(10 + 20)

# Interpolate symbol
var = :myvar
expr = :($var = 100)
println(expr)  # :(myvar = 100)

# Interpolate expression
sub_expr = :(a + b)
expr = :($sub_expr * c)
println(expr)  # :((a + b) * c)

eval and Execution

julia
# eval executes expression in global scope
expr = :(x = 10)
eval(expr)
println(x)  # 10

# Evaluate expression
result = eval(:(2^10))
println(result)  # 1024

# Execute code block
eval(quote
    a = 5
    b = 10
    println(a + b)
end)
# Output: 15

# Dynamically generate code
for op in (:+, :-, :*, :/)
    @eval function calc(a, b, ::Val{$(QuoteNode(op))})
        return $op(a, b)
    end
end

Macros

Macro Basics

julia
# Define macro
macro sayhello(name)
    return :(println("Hello, ", $name, "!"))
end

# Call macro
@sayhello "Julia"  # Hello, Julia!

# Macro receives expression, returns expression
macro show_expr(expr)
    println("Expression: ", expr)
    return expr
end

@show_expr 1 + 2  # Prints expression, returns 3

Macro Expansion

julia
macro debug(expr)
    return quote
        println("Expression: ", $(string(expr)))
        println("Result: ", $expr)
    end
end

@debug 1 + 2
# Expression: 1 + 2
# Result: 3

# View macro expansion
println(@macroexpand @debug 1 + 2)

Hygienic Macros

Julia's macros are "hygienic", automatically handling variable name conflicts:

julia
macro make_var()
    return quote
        x = 100  # This x is macro-internal
    end
end

x = 10
@make_var
println(x)  # 10 (external x unaffected)

# Use esc to break hygiene
macro make_global_var()
    return :($(esc(:x)) = 100)
end

x = 10
@make_global_var
println(x)  # 100 (external x modified)

Common Macro Patterns

julia
# Timing macro
macro timed(expr)
    return quote
        local start = time()
        local result = $(esc(expr))
        local elapsed = time() - start
        println("Elapsed: $(elapsed) seconds")
        result
    end
end

@timed begin
    sleep(1)
    42
end

# Conditional compilation
macro ifelse_example(condition, true_expr, false_expr)
    return quote
        if $(esc(condition))
            $(esc(true_expr))
        else
            $(esc(false_expr))
        end
    end
end

# Logging macro
macro log(level, message)
    return quote
        println("[$(uppercase(string($level)))] $($message)")
    end
end

@log :info "Application started"

Generated Functions

@generated Functions

julia
@generated function mysum(x::NTuple{N, T}) where {N, T}
    # Generate code at compile time
    expr = :(x[1])
    for i in 2:N
        expr = :($expr + x[$i])
    end
    return expr
end

println(mysum((1, 2, 3)))      # 6
println(mysum((1, 2, 3, 4)))   # 10

Type-Specialized Code Generation

julia
@generated function type_info(::Type{T}) where T
    # Generate different code based on type
    if T <: Integer
        return :(println("This is an integer type"))
    elseif T <: AbstractFloat
        return :(println("This is a float type"))
    else
        return :(println("This is another type"))
    end
end

type_info(Int64)     # This is an integer type
type_info(Float64)   # This is a float type
type_info(String)    # This is another type

Common Built-in Macros

@show

julia
x = 10
y = 20
@show x        # x = 10
@show x + y    # x + y = 30
@show x, y     # (x, y) = (10, 20)

@time and @elapsed

julia
@time begin
    sum(rand(1000000))
end
# Outputs time and memory allocation info

elapsed = @elapsed sum(rand(1000000))
println("Elapsed: $(elapsed) seconds")

@assert

julia
x = 10
@assert x > 0 "x must be positive"
# @assert x < 0 "x must be negative"  # Would error

@inline and @noinline

julia
@inline function fast_add(a, b)
    return a + b
end

@noinline function no_inline_add(a, b)
    return a + b
end

@simd and @threads

julia
# SIMD vectorization
function sum_simd(arr)
    s = zero(eltype(arr))
    @simd for x in arr
        s += x
    end
    return s
end

# Multithreading
using Base.Threads
function parallel_sum(arr)
    n = length(arr)
    partial = zeros(eltype(arr), nthreads())
    @threads for i in 1:n
        partial[threadid()] += arr[i]
    end
    return sum(partial)
end

Practical Examples

Auto-generate Getter/Setter

julia
macro auto_accessors(struct_name, fields...)
    getters = []
    setters = []
    
    for field in fields
        getter = Symbol("get_", field)
        setter = Symbol("set_", field)
        
        push!(getters, quote
            $(esc(getter))(obj::$(esc(struct_name))) = obj.$field
        end)
        
        push!(setters, quote
            function $(esc(setter))(obj::$(esc(struct_name)), val)
                obj.$field = val
            end
        end)
    end
    
    return quote
        $(getters...)
        $(setters...)
    end
end

mutable struct Person
    name::String
    age::Int
end

@auto_accessors Person name age

p = Person("Alice", 30)
println(get_name(p))  # Alice
set_age(p, 31)
println(get_age(p))   # 31

Domain Specific Language (DSL)

julia
# Simple test DSL
macro test_suite(name, tests)
    return quote
        println("Running test suite: ", $name)
        $(esc(tests))
        println("Tests complete")
    end
end

macro it(description, body)
    return quote
        print("  Test: ", $description, " ... ")
        try
            $(esc(body))
            println("✓")
        catch e
            println("✗")
            println("    Error: ", e)
        end
    end
end

@test_suite "Math operations" begin
    @it "addition works" begin
        @assert 1 + 1 == 2
    end
    
    @it "multiplication works" begin
        @assert 2 * 3 == 6
    end
end

SQL Query Builder

julia
macro select(fields, table)
    field_str = join([string(f) for f in fields.args], ", ")
    return :(println("SELECT $($field_str) FROM $($table)"))
end

@select((id, name, email), users)
# Output: SELECT id, name, email FROM users

Debugging Tips

julia
# View macro expansion
@macroexpand @show x

# View generated code
@code_lowered sum([1,2,3])
@code_typed sum([1,2,3])
@code_llvm sum([1,2,3])
@code_native sum([1,2,3])

# Print expression tree
Meta.show_sexpr(:(x + y * z))

Best Practices

  1. Use macros sparingly: Only use macros when necessary, prefer functions
  2. Keep macros simple: Put complex logic in helper functions
  3. Use esc correctly: Escape expressions that need evaluation in caller's scope
  4. Test macro expansion: Use @macroexpand to verify generated code
  5. Document macros: Provide clear documentation for macros

Next Steps

After learning metaprogramming, you've mastered Julia's core features!

Continue exploring:

  • Julia's parallel computing
  • Package development
  • Performance optimization techniques
  • Interoperability with other languages

Congratulations on completing the Julia tutorial!

Content is for learning and research only.