我想创建一个为我创建代码的宏。 E.g。
我有一个向量x= [9,8,7]
,我想使用宏来生成这段代码vcat(x[1], x[2], x[3])
并运行它。我希望它适用于任意长度的向量。
我已经制作了如下的宏
macro some_macro(a)
quote
astr = $(string(a))
s = mapreduce(aa -> string(astr,"[",aa,"],"), string, 1:length($(a)))
eval(parse(string("vcat(", s[1:(end-1)],")")))
end
end
x = [7,8,9]
@some_macro x
以上作品。但是当我尝试将其包装在函数中时
function some_fn(y)
@some_macro y
end
some_fn([4,5,6])
它不起作用并给出错误
UndefVarError:y未定义
它突出了以下作为罪魁祸首
s = mapreduce(aa -> string(astr,"[",aa,"],"), string, 1:length($(a)))
修改 见julia: efficient ways to vcat n arrays
高级示例为什么我要做而不是使用splat运算符
答案 0 :(得分:3)
您实际上并不需要宏或生成的函数。只需使用vcat(x...)
即可。这三个点是"splat" operator - 它解包了x
的所有元素,并将每个元素作为单独的参数传递给vcat
。
编辑:更直接地回答问题:这不能在宏中完成。宏在解析时扩展,但这种转换需要您知道数组的长度。在全局范围和简单测试中,可能看起来它正在工作,但它只是起作用,因为参数是在解析时定义的。然而,在一个函数或任何实际用例中,情况并非如此。在宏中使用eval
是主要红旗,实际上不应该这样做。
这是一个演示。您可以安全轻松地创建vcat
三个参数的宏。请注意,您不应该在此处构造“代码”字符串,您只需使用:( )
表达式引用语法构造表达式数组:
julia> macro vcat_three(x)
args = [:($(esc(x))[$i]) for i in 1:3]
return :(vcat($(args...)))
end
@vcat_three (macro with 1 method)
julia> @macroexpand @vcat_three y
:((Main.vcat)(y[1], y[2], y[3]))
julia> f(z) = @vcat_three z
f([[1 2], [3 4], [5 6], [7 8]])
3×2 Array{Int64,2}:
1 2
3 4
5 6
这样就可以了;我们esc(x)
得到正确的卫生,并将表达式数组直接映射到vcat
调用,以便在分析时生成该参数列表。它高效而快速。但现在让我们尝试将其扩展为支持length(x)
个参数。应该够简单。我们只需要将1:3
更改为1:n
,其中n
是数组的长度。
julia> macro vcat_n(x)
args = [:($(esc(x))[$i]) for i in 1:length(x)]
return :(vcat($(args...)))
end
@vcat_n (macro with 1 method)
julia> @macroexpand @vcat_n y
ERROR: LoadError: MethodError: no method matching length(::Symbol)
但这不起作用 - x
只是宏的符号,当然length(::Symbol)
并不代表我们想要的东西。事实证明,没有什么可以放在那里有效,只是因为Julia无法知道编译时有多大x
。
您的尝试失败,因为您的宏返回一个表达式,该表达式在运行时构造并eval
是一个字符串,eval
does not work in local scopes。即使它可以起作用,它也会非常缓慢......比喷溅慢得多。
如果您想使用更复杂的表达式执行此操作,则可以展开生成器:vcat((elt[:foo] for elt in x)...)
。
答案 1 :(得分:2)
FWIW,这是我在评论中提到的@generated
版本:
@generated function vcat_something(x, ::Type{Val{N}}) where N
ex = Expr(:call, vcat)
for i = 1:N
push!(ex.args, :(x[$i]))
end
ex
end
julia> vcat_something(x, Val{length(x)})
5-element Array{Float64,1}:
0.670889
0.600377
0.218401
0.0171423
0.0409389
您还可以删除@generated
前缀以查看它返回的Expr
:
julia> vcat_something(x, Val{length(x)})
:((vcat)(x[1], x[2], x[3], x[4], x[5]))
看看下面的基准测试结果:
julia> using BenchmarkTools
julia> x = rand(100)
julia> @btime some_fn($x)
190.693 ms (11940 allocations: 5.98 MiB)
julia> @btime vcat_something($x, Val{length(x)})
960.385 ns (101 allocations: 2.44 KiB)
巨大的性能差距主要是由于@generated
函数首先在编译时(在类型推断阶段之后)执行并执行一次,因为传递给它的每个N
。当使用具有相同长度x
的向量N
调用它时,它不会运行for循环,而是直接运行专门的编译代码/ Expr:< / p>
julia> x = rand(77); # x with a different length
julia> @time some_fn(x);
0.150887 seconds (7.36 k allocations: 2.811 MiB)
julia> @time some_fn(x);
0.149494 seconds (7.36 k allocations: 2.811 MiB)
julia> @time vcat_something(x, Val{length(x)});
0.061618 seconds (6.25 k allocations: 359.003 KiB)
julia> @time vcat_something(x, Val{length(x)});
0.000023 seconds (82 allocations: 2.078 KiB)
请注意,我们需要将x的长度传递给它ala一个值类型(Val
),因为Julia无法获取该信息(与NTuple
不同,{{1}在编译时只有一个类型参数。
编辑: 请参阅Matt的答案,找到解决问题的正确和最简单的方法,我会在这里留下相关信息,因为它有用,在处理splatting penalty时可能会有所帮助。