强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Julia 教程 / Julia 宏与元编程

12. 宏与元编程

宏(Macros)是 Julia 元编程的核心工具,能够在编译时对代码进行变换和生成,实现零成本抽象。

12.1 宏定义基础

macro/end 语法

# 最简单的宏
macro sayhello()
    return :(println("Hello, Julia!"))
end

@sayhello  # Hello, Julia!

带参数的宏

macro say(name)
    return :(println("Hello, ", $name, "!"))
end

@say "World"    # Hello, World!
@say "Julia"    # Hello, Julia!

# 多参数
macro twostep(a, b)
    return quote
        println("Step 1: ", $a)
        println("Step 2: ", $b)
    end
end

@twostep "读取数据" "处理数据"

宏的返回值

# 宏返回的是表达式,会在调用处展开
macro identity(expr)
    return esc(expr)
end

# 与直接写 expr 效果相同
result = @identity 2 + 3
println(result)  # 5

12.2 @macroexpand:宏展开

macro add(a, b)
    return :($a + $b)
end

# 查看宏展开后的代码
println(@macroexpand @add 1 2)
# 输出: 1 + 2

# 更复杂的例子
macro swap(a, b)
    return quote
        $a, $b = $b, $a
    end
end

println(@macroexpand @swap x y)
# 输出: begin
#     x, y = y, x
# end

x, y = 1, 2
@swap x y
println("x=$x, y=$y")  # x=2, y=1

💡 调试技巧:开发宏时始终用 @macroexpand 检查展开结果,确保符合预期。

12.3 表达式与 QuoteNode

Julia AST(抽象语法树)

# 表达式的类型层次
# Expr   — 复合表达式 (含 head 和 args)
# Symbol — 标识符
# Number, String 等 — 字面量

# 创建表达式的两种方式
expr1 = :(x + y)          # quote ... end 短形式
expr2 = quote
    x + y
end

# 查看表达式结构
dump(:(x + y))
# Expr
#   head: Symbol call
#   args: Array{Any}((3,))
#     1: Symbol +
#     2: Symbol x
#     3: Symbol y

# 手动构建表达式
expr3 = Expr(:call, :+, :x, :y)
println(:(x + y) == expr3)  # true

QuoteNode

# QuoteNode 防止表达式被继续求值
macro wrap(expr)
    qn = QuoteNode(expr)
    return :(println("表达式: ", $qn))
end

@wrap x + y  # 输出: 表达式: x + y

12.4 $ 插值与 esc

$ 插值:在 quote 块中插入值

macro show_expr(expr)
    str = string(expr)
    return :(println("表达式 $str = ", $expr))
end

@show_expr 2 + 3       # 表达式 2 + 3 = 5
@show_expr sin(π/4)    # 表达式 sin(π / 4) = 0.7071067811865475

esc:卫生宏的关键

# ❌ 错误示例:宏内部变量污染调用环境
macro bad_define()
    x = 42
    return :(result = $x)
end
# @bad_define 会覆盖调用者作用域中的 result

# ✅ 正确示例:使用 esc
macro good_define()
    x = 42
    return esc(:(result = $x))
end

# esc 将表达式"转义"到调用者的作用域中执行
@macroexpand @good_define

什么时候用 esc

# 规则:宏参数通常需要 esc,宏内部生成的新变量不需要
macro double(ex)
    return :($ex + $ex)  # ex 来自调用者,需要 $
end

macro with_tempvar(ex)
    temp = gensym("temp")  # 生成唯一符号避免冲突
    return esc(:($temp = 100; $ex($temp)))
end

⚠️ 常见错误:忘记 esc 会导致变量名冲突。始终用 @macroexpand 检查宏展开。

12.5 常见内置宏

功能示例
@show打印表达式和值@show x + 1
@time计时@time sum(1:1000)
@elapsed返回运行时间t = @elapsed sleep(1)
@assert断言@assert x > 0 "必须为正"
@inbounds关闭边界检查@inbounds a[1]
@simd向量化提示@simd for i in 1:n
@fastmath允许数学优化@fastmath sqrt(x)
@inline内联提示@inline f(x) = x^2
@nospecialize避免特化@nospecialize f(x) = x
@deprecate标记废弃@deprecate old new

实用示例

# @show - 调试神器
x, y = 10, 20
@show x y x + y
# x = 10
# y = 20
# x + y = 30

# @assert - 参数验证
function sqrt_pos(x)
    @assert x >= 0 "输入必须非负,收到: $x"
    return sqrt(x)
end
# sqrt_pos(-1)  # AssertionError: 输入必须非负,收到: -1

# @time - 性能计时
@time sum(sin(x) for x in 1:10_000_000)

# @inbounds - 高性能数组操作
function sum_array(a)
    s = zero(eltype(a))
    @inbounds for i in eachindex(a)
        s += a[i]
    end
    return s
end

12.6 代码生成:generate 函数

# 用宏生成一组函数
macro define_arithmetic_ops(T)
    esc(quote
        Base.:+(a::$T, b::$T) = $T(a.val + b.val)
        Base.:-(a::$T, b::$T) = $T(a.val - b.val)
        Base.:*(a::$T, b::$T) = $T(a.val * b.val)
        Base.display(a::$T) = print("$(a.val)")
    end)
end

struct Meter
    val::Float64
end
@define_arithmetic_ops Meter

# 现在可以直接使用运算符
a = Meter(1.5)
b = Meter(2.3)
println((a + b).val)  # 3.8

生成函数族

# 批量生成多个相关函数
for (func, op) in [(:my_add, :+), (:my_sub, :-), (:my_mul, :*)]
    @eval begin
        $func(a, b) = $op(a, b)
    end
end

println(my_add(3, 4))  # 7
println(my_sub(3, 4))  # -1
println(my_mul(3, 4))  # 12

12.7 宏 vs 函数选择

场景推荐原因
需要访问调用位置函数无法获得源代码信息
修改语法只有宏能变换代码结构
编译时优化零运行时开销
普通计算函数更容易测试和调试
运行时决策函数宏在编译时运行
DSL 定义语法变换是宏的强项
# ✅ 适合用宏:需要源代码信息
macro location()
    return QuoteNode(__source__)
end

loc = @location
println("定义于 ", loc)

# ✅ 适合用函数:纯计算
add(a, b) = a + b

12.8 卫生宏(Hygiene)

卫生宏确保宏内部的变量不会与调用者环境冲突。

# Julia 默认尝试做卫生处理
macro make_counter()
    return quote
        count = 0        # Julia 会重命名这个变量
        () -> (count += 1; count)
    end
end

counter = @make_counter
println(counter())  # 1
println(counter())  # 2

gensym:手动创建唯一符号

macro safe_let(ex)
    var = gensym("var")
    return esc(:(
        let $var = 42
            $var + $ex
        end
    ))
end

# 即使外部有同名变量也不会冲突
var = 100
println(@safe_let 8)  # 50
println(var)           # 100,未被修改

⚠️ 卫生注意:如果宏需要修改调用者环境中的变量,必须使用 esc。默认情况下 Julia 尝试保持卫生。

12.9 宏调试技巧

方法一:@macroexpand

macro buggy(x)
    return :($x + y)  # y 未定义!
end

# 先看展开结果
expanded = @macroexpand @buggy 5
println(expanded)
# 发现 y 是自由变量,修复宏

方法二:@macroexpand1

# 只展开一层宏(嵌套宏时有用)
macro outer()
    return :(@inner 42)
end

# @macroexpand 会展开所有层
# @macroexpand1 只展开最外层

方法三:宏测试

using Test

macro inc(x)
    return esc(:($x += 1))
end

@testset "inc 宏测试" begin
    a = 10
    @inc a
    @test a == 11

    b = 0.0
    @inc b
    @test b == 1.0
end

12.10 DSL 构建入门

构建简单的配置 DSL

macro config(ex)
    # 假设 ex 是一个 begin...end 块
    assignments = []
    for arg in ex.args
        if arg isa Expr && arg.head == :(=)
            key = QuoteNode(arg.args[1])
            val = arg.args[2]
            push!(assignments, :($key => $val))
        end
    end
    return esc(:(Dict($(assignments...))))
end

cfg = @config begin
    host = "localhost"
    port = 8080
    debug = true
    max_retries = 3
end

println(cfg["host"])  # localhost
println(cfg["port"])  # 8080

构建时间测量 DSL

macro timed_block(label, block)
    return quote
        local start_time = time()
        local result = $block
        local elapsed = time() - start_time
        println("[$label] 耗时: $(round(elapsed, digits=4))秒")
        result
    end
end

# 使用
@timed_block "数据处理" begin
    data = rand(10_000_000)
    s = sum(data)
    m = mean(data)
end

12.11 实际业务场景

场景一:日志记录宏

macro log_call(ex)
    func_name = string(ex.head == :call ? ex.args[1] : ex)
    args_str = string(ex)
    return quote
        println("[LOG] 调用: ", $args_str)
        local result = $ex
        println("[LOG] 结果: ", result)
        result
    end
end

# 使用
@log_call sin(π/6)
@log_call gcd(12, 8)

场景二:参数验证宏

macro require_positive(args...)
    checks = []
    for arg in args
        name = string(arg)
        push!(checks, :(
            @assert $arg > 0 "参数 $($(name)) 必须为正数,收到: $($arg)"
        ))
    end
    return esc(quote
        $(checks...)
    end
end

function calculate_rate(principal, rate, years)
    @require_positive principal rate years
    return principal * (1 + rate)^years
end

# calculate_rate(1000, -0.05, 10)  # AssertionError
println(calculate_rate(1000, 0.05, 10))  # 1628.894626777441

场景三:单元测试简化宏

macro test_approx(a, b, tol=1e-6)
    return quote
        local diff = abs($a - $b)
        if diff > $tol
            error("近似相等测试失败: |$($a) - $($b)| = $diff > $($tol)")
        end
    end
end

@test_approx sin(π/6) 0.5
@test_approx 0.1 + 0.2 0.3 1e-15

12.12 扩展阅读

资源链接
Julia 官方文档 - Metaprogramminghttps://docs.julialang.org/en/v1/manual/metaprogramming/
MacroTools.jlhttps://github.com/FluxML/MacroTools.jl
ExprTools.jlhttps://github.com/invenia/ExprTools.jl
元编程最佳实践https://discourse.julialang.org/t/macro-best-practices/
Julia AST 参考https://docs.julialang.org/en/v1/devdocs/ast/

12.13 本章小结

主题要点
宏定义macro name(...) ... end
宏展开@macroexpand 查看编译后的代码
表达式:(...) / quote ... end 创建 AST
插值$ 在 quote 中插入值
卫生esc 转义到调用者作用域
调试@macroexpand + 单元测试
DSL宏是构建领域特定语言的基础