QuoteNode Meta.quot 和 Expr(引用)

使用 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> 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

第二種情況是不正確的,因為我們應該保持這種情況不被評估。我們通過引用 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 的一個限制是我們不能直接將表示式用作表示式的左側和右側,因為它們被插值。換句話說,可以對表示式進行插值評估,但我們可能希望保留它們。 (由於這裡有很多級別的引用和評估,讓我們澄清一下:我們的巨集生成的程式碼構造了一個表示式,當計算得到另一個表示式時 .Phew!)

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(:引用)呢?

Expr(:quote, x) 相當於 Meta.quot(x)。然而,後者更具慣用性並且是優選的。對於大量使用超程式設計的程式碼,經常使用 using Base.Meta 線,這使得 Meta.quot 可以簡稱為 quot