【问题标题】:Julia scope of macro variables and eval宏变量和 eval 的 Julia 范围
【发布时间】:2017-09-27 12:18:51
【问题描述】:

我想制作一个为我创建一些代码的宏。例如

我有一个向量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 运算符

【问题讨论】:

  • 你需要escape a:astr = string($(esc(a))) 对第二行做同样的修改。
  • 您检查过@generated 函数吗?看来这才是你真正需要的,而不是宏。

标签: julia


【解决方案1】:

您并不需要为此使用宏或生成的函数。只需使用vcat(x...)。这三个点是"splat" operator——它解包x 的所有元素,并将每个元素作为单独的参数传递给vcat

编辑:更直接地回答所提出的问题:这不能在宏中完成。宏在解析时扩展,但这种转换需要您知道数组的长度。在全局范围和简单的测试中,它可能看起来正在工作,但它只是工作,因为参数是在解析时定义的。然而,在函数或任何实际用例中,情况并非如此。在宏中使用eval 是一个重要的危险信号,确实不应该这样做。

这是一个演示。您可以安全轻松地创建一个包含vcats 三个参数的宏。请注意,您根本不应该在这里构造“代码”字符串,您可以使用 :( ) 表达式引用语法构造一个表达式数组:

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 在编译时有多大。

您的尝试失败了,因为您的宏返回一个表达式,该表达式在运行时构造和evals 一个字符串,以及eval does not work in local scopes。即使这样可行,它也会非常缓慢……比飞溅慢得多。


如果您想使用更复杂的表达式来执行此操作,可以使用生成器:vcat((elt[:foo] for elt in x)...)

【讨论】:

    【解决方案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 函数首先执行并且只执行一次。当用向量xN 相同长度调用它时,它不会运行for循环,而是直接运行专门的编译代码/Expr:

    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 的长度作为值类型(Val)传递给它,因为 Julia 无法获取该信息(与 NTuple 不同,Vector 只有一个类型参数)编译时间。

    编辑: 请参阅马特的回答,了解解决问题的正确和最简单的方法,我将把帖子留在这里,因为它是相关的,并且在处理 splatting penalty 时可能会有所帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-07-07
      • 1970-01-01
      • 1970-01-01
      • 2020-09-24
      • 2012-09-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多