从Julia中生成的函数中调用宏



我一直在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))像预期的那样工作,并返回表达式
:(@my_add 2.0 1.0)

正确计算3.0

但是计算add_one_gen(2.0)返回以下错误:

MethodError: no method matching +(::Type{Float64}, ::Float64)

做一些研究,我发现@generated实际上产生了两个代码,在一个只有变量的类型可以使用。我认为这就是这里正在发生的事情,但我根本不明白发生了什么。在宏和生成的函数之间一定有一些奇怪的交互。

有人能解释和/或提出解决方案吗?谢谢你!

我认为生成的函数有两个组件是有帮助的:主体和任何生成的代码(quote..end内部的东西)。函数体是在编译时求值的,并不"知道"。只有值,只有类型。因此,对于以x::T为参数的生成函数,函数体中对x的任何引用实际上都指向类型T。这可能非常令人困惑。为了使事情更清楚,我建议正文指代类型,而不是值。

这里有一个小例子:

julia> @generated function show_val_and_type(x::T) where {T}
quote
println("x is ", x)
println("$x is ", $x)
println("T is ", T)
println("$T is ", $T)
end
end
show_val_and_type
julia> show_val_and_type(3)
x is 3
$x is Int64
T is Int64
$T is Int64

插入的$x意味着"从主体中取出x(指T)并将其拼接进去。

如果遵循从不引用代码体中的值的方法,可以通过删除@generated来测试生成的函数,如下所示:

julia> function add_one_gen(x::T) where T
y = one(T)
quote
@my_add(x,$y)
end
end
add_one_gen
julia> add_one_gen(3)
quote
#= REPL[42]:4 =#
#= REPL[42]:4 =# @my_add x 1
end

这看起来很合理,但是当我们测试它时,我们得到

julia> add_one_gen(3)
ERROR: UndefVarError: x not defined
Stacktrace:
[1] macro expansion
@ ./REPL[48]:4 [inlined]
[2] add_one_gen(x::Int64)
@ Main ./REPL[48]:1
[3] top-level scope
@ REPL[49]:1

让我们看看宏给了我们什么

julia> @macroexpand @my_add x 1
:(Main.x + 1)

指向不存在的Main.x。宏观太急于求成了,我们需要推迟对它的评估。这样做的标准方法是使用esc。最后,这是可行的:

julia> macro my_add(a,b) 
return :($(esc(a)) + $(esc(b)))
end
@my_add
julia> @generated function add_one_gen(x::T) where T
y = one(T)
quote
@my_add(x,$y)
end
end
add_one_gen
julia> add_one_gen(3)
4

最新更新