Julia范围的宏变量和eval

时间:2017-09-27 12:18:52

标签: julia

我想创建一个为我创建代码的宏。 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运算符

2 个答案:

答案 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时可能会有所帮助。