Julia中在生成函数内调用宏报错的原因与解决方案
Julia生成函数内部调用宏触发MethodError的原因与解决方案
问题场景
在Julia生成函数内部调用宏(如张量缩并库的@tullio宏),尝试实现依赖输入类型的张量操作时,宏展开行为不符合预期,触发方法不存在的错误。
复现示例
# 定义测试宏 macro my_add(a,b) return :($a + $b) end # 普通返回表达式的函数 function add_one_expr(x::T) where T y = one(T) return :( @my_add($x,$y) ) end # 待测试的生成函数 @generated function add_one_gen(x::T) where T y = one(T) return :( @my_add($x,$y) ) end
现象对比
- 执行
eval(add_one_expr(2.0))可正常返回结果3.0,该调用返回的表达式为:(@my_add 2.0 1.0),求值逻辑符合预期。 - 直接执行
add_one_gen(2.0)时抛出如下错误:
MethodError: no method matching +(::Type{Float64}, ::Float64)
根本原因
核心问题是对Julia生成函数的两阶段执行逻辑、以及表达式插值$的作用时机理解偏差,和宏本身的特殊交互无关:
- 生成函数存在两个完全隔离的执行阶段:
- 编译生成阶段:函数第一次被某类型参数调用时触发,此时传入形参的是参数的类型本身,不是实际运行时的值,该阶段的执行逻辑只能依赖参数类型,最终返回一个待执行的Julia表达式。
- 运行执行阶段:编译阶段返回的表达式会被插入到调用位置,和普通手写代码一样完成宏展开、求值操作,此时才能访问到传入的实际参数值。
- 表达式插值
$的生效时机是当前执行的阶段:- 普通函数
add_one_expr是运行时执行的,调用时x绑定到实际值2.0,因此:(@my_add($x, $y))插值后得到的是包含实际值的:(@my_add 2.0 1.0),求值正常。 - 生成函数里的返回表达式是在编译生成阶段构造的,此时
x绑定的是参数类型Float64,代码中写的$x会直接把类型对象插值到表达式里,最终返回给运行阶段的表达式实际是:(@my_add Float64 1.0),宏展开后变成Float64 + 1.0,自然会触发「类型对象无法和浮点数相加」的错误。
- 普通函数
- 额外注意:生成函数返回的quoted表达式里的宏,不会在编译生成阶段展开,只会等到运行阶段、表达式被插入调用上下文时才会展开,这个机制本身没有问题,错误的根源是错误地把仅在编译阶段存在的类型对象,通过插值拼进了最终运行的表达式里。
解决方案
根据使用场景选择对应方案即可,对于@tullio这类张量操作宏,优先使用第一种方案:
- 方案1:正确区分编译期常量和运行时参数,仅对编译阶段可确定的值加
$插值,运行时才能拿到的参数(如输入张量、数值参数)不要加$,直接作为表达式里的自由变量保留,修正后的代码如下:
@generated function add_one_gen_fix(x::T) where T y = one(T) # y是编译期根据类型就能确定的常量,需要插值 # 注意x前面没有$,会作为运行时的参数引用保留在表达式里 return :( @my_add(x, $y) ) end
执行add_one_gen_fix(2.0)会正常返回3.0,符合预期。
- 方案2:如果确实需要在编译生成阶段就完成宏展开,可以在生成阶段显式调用
macroexpand或者@eval完成宏展开,直接返回展开后的原始表达式:
@generated function add_one_gen_fix2(x::T) where T y = one(T) # 编译阶段直接展开宏,生成最终的运算表达式 return macroexpand(@__MODULE__, :(@my_add(x, $y))) end
该方案适合需要在编译期对宏生成的代码做进一步改写的场景,常规张量运算场景下方案1的可读性和维护性更好。
内容的提问来源于stack exchange,提问作者Jordi Manyer Fuertes
相关产品推荐
相关产品推荐

