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

# 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)

# 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

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

# 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

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

# 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

# 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

# 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

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:

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

# 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

@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

@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

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

@time and @elapsed

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

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

@assert

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

@inline and @noinline

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

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

@simd and @threads

# 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

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)

# 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

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

# 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!