【问题标题】:How to create a macro for creating a symbol in Julia如何在 Julia 中创建用于创建符号的宏
【发布时间】:2014-10-28 02:59:27
【问题描述】:

我正在尝试在 Julia 中创建 string literal macro 以创建 symbol,以便 s"x":x 相同。它不起作用:

julia> macro s_str(p)
           symbol(p)
       end

julia> s'x'
ERROR: s not defined

julia> s"x"
ERROR: x not defined

【问题讨论】:

    标签: macros julia


    【解决方案1】:

    原因是macro hygiene。你可以做任何一个

    macro s_str(p)
      quote
        symbol($p)
      end
    end
    

    这很容易阅读,或者做更复杂但等效的。

    macro s_str(p)
      esc(:(symbol($p)))
    end
    

    【讨论】:

    • 啊,所以esc 实际上根本不需要。
    • 第二个更复杂的例子不就等同于quot吗?
    【解决方案2】:

    我会推荐

    macro s_str(p)
        Meta.quot(Symbol(p))
    end
    

    这避免了对Symbol 的运行时调用。有关在宏中引用符号的更多信息,请参见下文。



    使用 Julia 函数可以通过三种方式引用某些内容:

    julia> QuoteNode(:x)
    :(:x)
    
    julia> Meta.quot(:x)
    :(:x)
    
    julia> Expr(:quote, :x)
    :(:x)
    

    “引用”是什么意思,它有什么用?引用允许我们保护表达式不被 Julia 解释为特殊形式。一个常见的用例是当我们生成应该包含评估为符号的内容的表达式时。 (例如,这个宏需要返回一个计算结果为符号的表达式。)它不能简单地返回符号:

    julia> macro mysym(); :x; end
    @mysym (macro with 1 method)
    
    julia> @mysym
    ERROR: UndefVarError: x not defined
    
    julia> macroexpand(:(@mysym))
    :x
    

    这里发生了什么? @mysym 扩展为:x,它作为一个表达式被解释为变量x。但是尚未将任何内容分配给 x,因此我们收到了 x not defined 错误。

    要解决这个问题,我们必须引用宏的结果:

    julia> macro mysym2(); Meta.quot(:x); end
    @mysym2 (macro with 1 method)
    
    julia> @mysym2
    :x
    
    julia> macroexpand(:(@mysym2))
    :(:x)
    

    在这里,我们使用了Meta.quot 函数将我们的符号变成了带引号的符号,这就是我们想要的结果。

    Meta.quotQuoteNode 有什么区别,我应该使用哪个?在几乎所有情况下,差异并不重要。有时使用QuoteNode 而不是Meta.quot 可能更安全一些。然而,探索差异有助于了解 Julia 表达式和宏的工作原理。

    Meta.quotQuoteNode的区别,解释

    这是一个经验法则:

    • 如果您需要或想要支持插值,请使用Meta.quot
    • 如果您不能或不想允许插值,请使用QuoteNode

    简而言之,区别在于Meta.quot 允许在引用的事物中进行插值,而QuoteNode 保护其参数不受任何插值的影响。要理解插值,重要的是要提到 $ 表达式。 Julia 中有一种表达式称为$ 表达式。这些表达式允许转义。例如,考虑以下表达式:

    julia> ex = :( x = 1; :($x + $x) )
    quote 
        x = 1
        $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
    end
    

    计算时,此表达式将计算 1 并将其分配给 x,然后构造 _ + _ 形式的表达式,其中 _ 将替换为 x 的值。因此,其结果应该是 表达式 1 + 1(尚未计算,因此与 2 截然不同)。确实是这样:

    julia> eval(ex)
    :(1 + 1)
    

    现在假设我们正在编写一个宏来构建这些类型的表达式。我们的宏将接受一个参数,它将替换上面ex 中的1。当然,这个参数可以是任何表达式。这不是我们想要的:

    julia> macro makeex(arg)
               quote
                   :( x = $(esc($arg)); :($x + $x) )
               end
           end
    @makeex (macro with 1 method)
    
    julia> @makeex 1
    quote 
        x = $(Expr(:escape, 1))
        $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
    end
    
    julia> @makeex 1 + 1
    quote 
        x = $(Expr(:escape, 2))
        $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
    end
    

    第二种情况是不正确的,因为我们应该保持1 + 1不被评估。我们通过引用Meta.quot 的参数来解决这个问题:

    julia> macro makeex2(arg)
               quote
                   :( x = $$(Meta.quot(arg)); :($x + $x) )
               end
           end
    @makeex2 (macro with 1 method)
    
    julia> @makeex2 1 + 1
    quote 
        x = 1 + 1
        $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
    end
    

    宏观卫生不适用于引用的内容,因此在这种情况下转义是不必要的(实际上是不合法的)。

    如前所述,Meta.quot 允许插值。所以让我们尝试一下:

    julia> @makeex2 1 + $(sin(1))
    quote 
        x = 1 + 0.8414709848078965
        $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
    end
    
    julia> let q = 0.5
               @makeex2 1 + $q
           end
    quote 
        x = 1 + 0.5
        $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
    end
    

    从第一个示例中,我们看到插值允许我们内联sin(1),而不是让表达式成为文字sin(1)。第二个例子表明这个插值是在宏调用范围内完成的,而不是在宏自己的范围内。那是因为我们的宏实际上并没有评估任何代码。它所做的只是生成代码。代码的求值(进入表达式)在宏生成的表达式实际运行时完成。

    如果我们改用QuoteNode 会怎样?正如您可能猜到的那样,由于QuoteNode 完全阻止了插值的发生,这意味着它不会起作用。

    julia> macro makeex3(arg)
               quote
                   :( x = $$(QuoteNode(arg)); :($x + $x) )
               end
           end
    @makeex3 (macro with 1 method)
    
    julia> @makeex3 1 + $(sin(1))
    quote 
        x = 1 + $(Expr(:$, :(sin(1))))
        $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
    end
    
    julia> let q = 0.5
               @makeex3 1 + $q
           end
    quote 
        x = 1 + $(Expr(:$, :q))
        $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
    end
    
    julia> eval(@makeex3 $(sin(1)))
    ERROR: unsupported or misplaced expression $
     in eval(::Module, ::Any) at ./boot.jl:234
     in eval(::Any) at ./boot.jl:233
    

    在这个例子中,我们可能同意Meta.quot 提供了更大的灵活性,因为它允许插值。那么为什么我们会考虑使用QuoteNode?在某些情况下,我们可能实际上并不需要插值,而实际上需要文字 $ 表达式。什么时候会是可取的?让我们考虑一下@makeex 的泛化,我们可以在其中传递额外的参数来确定+ 符号的左侧和右侧:

    julia> macro makeex4(expr, left, right)
               quote
                   quote
                       $$(Meta.quot(expr))
                       :($$$(Meta.quot(left)) + $$$(Meta.quot(right)))
                   end
               end
           end
    @makeex4 (macro with 1 method)
    
    julia> @makeex4 x=1 x x
    quote  # REPL[110], line 4:
        x = 1 # REPL[110], line 5:
        $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
    end
    
    julia> eval(ans)
    :(1 + 1)
    

    我们实现@makeex4 的一个限制是我们不能直接将表达式用作表达式的左侧和右侧,因为它们会被插值。换句话说,表达式可能会被评估以进行插值,但我们可能希望保留它们。 (由于这里有许多级别的引用和评估,让我们澄清一下:我们的宏生成构造 表达式代码,当评估时会产生另一个 表达式. 呸!)

    julia> @makeex4 x=1 x/2 x
    quote  # REPL[110], line 4:
        x = 1 # REPL[110], line 5:
        $(Expr(:quote, :($(Expr(:$, :(x / 2))) + $(Expr(:$, :x)))))
    end
    
    julia> eval(ans)
    :(0.5 + 1)
    

    我们应该允许用户指定插值何时发生,何时不发生。从理论上讲,这很容易解决:我们只需删除应用程序中的$ 标志之一,让用户自己贡献。这意味着我们插入了用户输入的表达式的引用版本(我们已经引用并插入了一次)。这导致以下代码,由于引用和取消引用的多个嵌套级别,起初可能会有些混乱。尝试阅读并理解每次逃跑的目的。

    julia> macro makeex5(expr, left, right)
               quote
                   quote
                       $$(Meta.quot(expr))
                       :($$(Meta.quot($(Meta.quot(left)))) + $$(Meta.quot($(Meta.quot(right)))))
                   end
               end
           end
    @makeex5 (macro with 1 method)
    
    julia> @makeex5 x=1 1/2 1/4
    quote  # REPL[121], line 4:
        x = 1 # REPL[121], line 5:
        $(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :(1 / 2)))))) + $(Expr(:$, :($(Expr(:quote, :(1 / 4)))))))))
    end
    
    julia> eval(ans)
    :(1 / 2 + 1 / 4)
    
    julia> @makeex5 y=1 $y $y
    ERROR: UndefVarError: y not defined
    

    事情开始很好,但出了点问题。宏生成的代码试图在宏调用范围内插入y 的副本;但在宏调用范围内没有 y 的副本。我们的错误是允许在宏中使用第二个和第三个参数进行插值。要修复这个错误,我们必须使用QuoteNode

    julia> macro makeex6(expr, left, right)
               quote
                   quote
                       $$(Meta.quot(expr))
                       :($$(Meta.quot($(QuoteNode(left)))) + $$(Meta.quot($(QuoteNode(right)))))
                   end
               end
           end
    @makeex6 (macro with 1 method)
    
    julia> @makeex6 y=1 1/2 1/4
    quote  # REPL[129], line 4:
        y = 1 # REPL[129], line 5:
        $(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :(1 / 2)))))) + $(Expr(:$, :($(Expr(:quote, :(1 / 4)))))))))
    end
    
    julia> eval(ans)
    :(1 / 2 + 1 / 4)
    
    julia> @makeex6 y=1 $y $y
    quote  # REPL[129], line 4:
        y = 1 # REPL[129], line 5:
        $(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)))))))) + $(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)))))))))))
    end
    
    julia> eval(ans)
    :(1 + 1)
    
    julia> @makeex6 y=1 1+$y $y
    quote  # REPL[129], line 4:
        y = 1 # REPL[129], line 5:
        $(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :(1 + $(Expr(:$, :y)))))))) + $(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)))))))))))
    end
    
    julia> @makeex6 y=1 $y/2 $y
    quote  # REPL[129], line 4:
        y = 1 # REPL[129], line 5:
        $(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)) / 2)))))) + $(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)))))))))))
    end
    
    julia> eval(ans)
    :(1 / 2 + 1)
    

    通过使用QuoteNode,我们保护了我们的参数不被插值。由于QuoteNode 仅具有附加保护的效果,因此使用QuoteNode 永远不会有害,除非您需要插值。但是,了解其中的区别可以了解Meta.quot 在哪里以及为什么会是更好的选择。

    这个冗长的练习带有一个明显太复杂而无法在任何合理的应用程序中显示的示例。因此,我们证明了前面提到的以下经验法则:

    • 如果您需要或想要支持插值,请使用Meta.quot
    • 如果您不能或不想允许插值,请使用QuoteNode

    Expr(:quote) 呢?

    Expr(:quote, x) 等价于Meta.quot(x)。然而,后者更惯用,是首选。对于大量使用元编程的代码,通常使用 using Base.Meta 行,这允许将 Meta.quot 简称为 quot

    【讨论】:

      【解决方案3】:

      首先,注意在 Julia 中对字符串使用 " 而不是 '' 表示字符,但也表示转置,通过隐式乘法,意味着s'x' 被翻译为transpose(s)*transpose(x)s"x" 是正确的,实际上是在调用 s_str 宏。

      问题是由于hygene,在评估宏时会评估引用符号。 esc 将创建一个特别引用的表达式,该表达式在评估后保持引用:

      julia> esc(:x)
      :($(Expr(:escape, :x)))
      

      请注意,我仍然在此处引用 x,以便它保持未计算(否则您最终会在最终表达式中使用 x 而不是 :x

      在这里,您需要转义完整的symbol(p)。您需要在: 中使用括号。最后,使用$p 来评估p(否则p 最终会被转义为符号)。

      julia> macro s_str(p)
                 esc(:(symbol($p)))
             end
      
      julia> s"x"
      :x
      

      【讨论】:

        猜你喜欢
        • 2021-12-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-04-08
        相关资源
        最近更新 更多