當前位置: 首頁>>代碼示例 >>用法及示例精選 >>正文


Elixir Kernel.SpecialForms.quote用法及代碼示例


Elixir語言中 Kernel.SpecialForms.quote 相關用法介紹如下。

用法:

quote(opts, block)
(宏)

獲取任何表達式的表示。

例子

iex> quote do
...>   sum(1, 2, 3)
...> end
{:sum, [], [1, 2, 3]}

Elixir 的 AST(抽象語法樹)

任何 Elixir 代碼都可以使用 Elixir 數據結構來表示。 Elixir 宏的構建塊是一個包含三個元素的元組,例如:

{:sum, [], [1, 2, 3]}

上麵的元組表示對 sum 的函數調用,將 1、2 和 3 作為參數傳遞。元組元素是:

  • 元組的第一個元素總是一個原子或相同表示中的另一個元組。

  • 元組的第二個元素表示 metadata

  • 元組的第三個元素是函數調用的參數。第三個參數可能是一個原子,它通常是一個變量(或本地調用)。

除了上述元組之外,Elixir 還有一些文字也是其 AST 的一部分。這些文字在引用時會自行返回。他們是:

:sum         #=> Atoms
1            #=> Integers
2.0          #=> Floats
[1, 2]       #=> Lists
"strings"    #=> Strings
{key, value} #=> Tuples with two elements

任何其他值,例如映射或 four-element 元組,必須在被引入 AST 之前進行轉義 ( Macro.escape/1 )。

選項

  • :bind_quoted - 將綁定傳遞給宏。無論何時給出綁定, unquote/1 都會自動禁用。

  • :context - 設置分辨率上下文。

  • :generated - 將給定的塊標記為已生成,因此它不會發出警告。目前它僅適用於特殊形式(例如,您可以注釋 case 但不能注釋 if )。

  • :file - 將引用的表達式設置為具有給定文件。

  • :line - 將引用的表達式設置為具有給定的行。

  • :location - 當設置為 :keep 時,保留當前行和文件的引用。閱讀下麵的"Stacktrace information" 部分了解更多信息。

  • :unquote - 當 false 時,禁用取消引用。這意味著任何 unquote 調用都將在 AST 中保持原樣,而不是被 unquote 參數替換。例如:

    iex> quote do
    ...>   unquote("hello")
    ...> end
    "hello"
    iex> quote unquote: false do
    ...>   unquote("hello")
    ...> end
    {:unquote, [], ["hello"]}

引號和宏

quote/2 通常與宏一起用於代碼生成。作為練習,讓我們定義一個將數字乘以自身(平方)的宏。在實踐中,沒有理由定義這樣的宏(實際上這會被視為一種不好的做法),但它足夠簡單,可以讓我們專注於引號和宏的重要方麵:

defmodule Math do
  defmacro squared(x) do
    quote do
      unquote(x) * unquote(x)
    end
  end
end

我們可以這樣調用它:

import Math
IO.puts("Got #{squared(5)}")

起初,這個例子中沒有任何東西可以真正揭示它是一個宏。但是正在發生的事情是,在編譯時, squared(5) 變為 5 * 55 參數在生成的代碼中重複,我們可以在實踐中看到這種行為,因為我們的宏實際上有一個錯誤:

import Math
my_number = fn ->
  IO.puts("Returning 5")
  5
end
IO.puts("Got #{squared(my_number.())}")

上麵的示例將打印:

Returning 5
Returning 5
Got 25

注意“Returning 5”是如何被打印兩次的,而不是一次。這是因為宏接收一個表達式而不是一個值(這是我們在常規函數中所期望的)。這意味著:

squared(my_number.())

實際上擴展為:

my_number.() * my_number.()

它調用了兩次函數,解釋了為什麽我們得到了兩次打印的值!在大多數情況下,這實際上是出乎意料的行為,這就是為什麽在涉及宏時需要記住的第一件事就是不要多次取消引用相同的值。

讓我們修複我們的宏:

defmodule Math do
  defmacro squared(x) do
    quote do
      x = unquote(x)
      x * x
    end
  end
end

現在像以前一樣調用squared(my_number.()) 將隻打印一次該值。

事實上,這種模式非常普遍,以至於大多數時候您都希望將 bind_quoted 選項與 quote/2 一起使用:

defmodule Math do
  defmacro squared(x) do
    quote bind_quoted: [x: x] do
      x * x
    end
  end
end

:bind_quoted 將轉換為與上述示例相同的代碼。 :bind_quoted 可以在很多情況下使用並且被視為良好實踐,不僅因為它有助於防止我們遇到常見錯誤,還因為它允許我們利用宏公開的其他工具,例如在某些下麵的部分。

在我們完成這個簡短的介紹之前,您會注意到,即使我們在引用中定義了一個變量x

quote do
  x = unquote(x)
  x * x
end

當我們調用時:

import Math
squared(5)
x
** (CompileError) undefined variable x or undefined function x/0

我們可以看到x 沒有泄漏到用戶上下文。發生這種情況是因為 Elixir 宏是衛生的,我們將在下一節中詳細討論這個主題。

變量中的衛生

考慮以下示例:

defmodule Hygiene do
  defmacro no_interference do
    quote do
      a = 1
    end
  end
end

require Hygiene

a = 10
Hygiene.no_interference()
a
#=> 10

在上麵的示例中,a 返回 10,即使宏顯然將其設置為 1,因為宏中定義的變量不會影響執行宏的上下文。如果要在調用者的上下文中設置或獲取變量,您可以在var! 宏的幫助下完成:

defmodule NoHygiene do
  defmacro interference do
    quote do
      var!(a) = 1
    end
  end
end

require NoHygiene

a = 10
NoHygiene.interference()
a
#=> 1

你甚至不能訪問在同一個模塊中定義的變量,除非你明確地給它一個上下文:

defmodule Hygiene do
  defmacro write do
    quote do
      a = 1
    end
  end

  defmacro read do
    quote do
      a
    end
  end
end

Hygiene.write()
Hygiene.read()
** (RuntimeError) undefined variable a or undefined function a/0

為此,您可以顯式地將當前模塊範圍作為參數傳遞:

defmodule ContextHygiene do
  defmacro write do
    quote do
      var!(a, ContextHygiene) = 1
    end
  end

  defmacro read do
    quote do
      var!(a, ContextHygiene)
    end
  end
end

ContextHygiene.write()
ContextHygiene.read()
#=> 1

別名衛生

默認情況下,引號內的別名是衛生的。考慮以下示例:

defmodule Hygiene do
  alias Map, as: M

  defmacro no_interference do
    quote do
      M.new()
    end
  end
end

require Hygiene
Hygiene.no_interference()
#=> %{}

請注意,即使在宏展開的上下文中別名 M 不可用,上麵的代碼仍然有效,因為 M 仍然展開為 Map

同樣,即使我們在調用宏之前定義了同名的別名,也不會影響宏的結果:

defmodule Hygiene do
  alias Map, as: M

  defmacro no_interference do
    quote do
      M.new()
    end
  end
end

require Hygiene
alias SomethingElse, as: M
Hygiene.no_interference()
#=> %{}

在某些情況下,您希望訪問調用者中定義的別名或模塊。為此,您可以使用 alias! 宏:

defmodule Hygiene do
  # This will expand to Elixir.Nested.hello()
  defmacro no_interference do
    quote do
      Nested.hello()
    end
  end

  # This will expand to Nested.hello() for
  # whatever is Nested in the caller
  defmacro interference do
    quote do
      alias!(Nested).hello()
    end
  end
end

defmodule Parent do
  defmodule Nested do
    def hello, do: "world"
  end

  require Hygiene
  Hygiene.no_interference()
  ** (UndefinedFunctionError) ...

  Hygiene.interference()
  #=> "world"
end

導入衛生

與別名類似,Elixir 中的導入是衛生的。考慮以下代碼:

defmodule Hygiene do
  defmacrop get_length do
    quote do
      length([1, 2, 3])
    end
  end

  def return_length do
    import Kernel, except: [length: 1]
    get_length
  end
end

Hygiene.return_length()
#=> 3

請注意 Hygiene.return_length/0 如何返回 3,即使未導入 Kernel.length/1 函數。事實上,即使return_length/0 從另一個模塊導入同名同元的函數,也不會影響函數結果:

def return_length do
  import String, only: [length: 1]
  get_length
end

調用這個新的 return_length/0 仍將返回 3 作為結果。

Elixir 足夠聰明,可以將解決方案延遲到可能的最新時刻。因此,如果您在引號內調用 length([1, 2, 3]),但沒有可用的 length/1 函數,則它會在調用者中展開:

defmodule Lazy do
  defmacrop get_length do
    import Kernel, except: [length: 1]

    quote do
      length("hello")
    end
  end

  def return_length do
    import Kernel, except: [length: 1]
    import String, only: [length: 1]
    get_length
  end
end

Lazy.return_length()
#=> 5

堆棧跟蹤信息

通過宏定義函數時,開發人員可以選擇是從調用者還是從引用內部報告運行時錯誤。讓我們看一個例子:

# adder.ex
defmodule Adder do
  @doc "Defines a function that adds two numbers"
  defmacro defadd do
    quote location: :keep do
      def add(a, b), do: a + b
    end
  end
end

# sample.ex
defmodule Sample do
  import Adder
  defadd
end

require Sample
Sample.add(:one, :two)
** (ArithmeticError) bad argument in arithmetic expression
    adder.ex:5: Sample.add/2

當使用 location: :keep 並且為 Sample.add/2 提供無效參數時,堆棧跟蹤信息將指向引用內的文件和行。如果沒有 location: :keep ,則會將錯誤報告到調用 defadd 的位置。 location: :keep 僅影響引號內的定義。

重要提示:不要使用 location: :keep 如果函數定義還 unquote s 一些宏參數。如果這樣做,Elixir 將存儲當前位置的文件定義,但未引用的參數可能包含宏調用者的行信息,從而導致錯誤的堆棧跟蹤。

綁定和取消引用片段

Elixir 引用/取消引用機製提供了一種稱為取消引用片段的函數。取消引用片段提供了一種即時生成函數的簡單方法。考慮這個例子:

kv = [foo: 1, bar: 2]
Enum.each(kv, fn {k, v} ->
  def unquote(k)(), do: unquote(v)
end)

在上麵的示例中,我們動態生成了函數foo/0bar/0。現在,假設我們想將此函數轉換為宏:

defmacro defkv(kv) do
  Enum.map(kv, fn {k, v} ->
    quote do
      def unquote(k)(), do: unquote(v)
    end
  end)
end

我們可以調用這個宏:

defkv [foo: 1, bar: 2]

但是,我們不能如下調用它:

kv = [foo: 1, bar: 2]
defkv kv

這是因為宏在編譯時期望它的參數是一個關鍵字列表。由於在上麵的示例中,我們傳遞了變量 kv 的表示形式,因此我們的代碼失敗了。

這實際上是開發宏時的常見陷阱。我們在宏中假設一個特定的形狀。我們可以通過在引用的表達式中取消引用變量來解決它:

defmacro defkv(kv) do
  quote do
    Enum.each(unquote(kv), fn {k, v} ->
      def unquote(k)(), do: unquote(v)
    end)
  end
end

如果您嘗試運行我們的新宏,您會發現它甚至無法編譯,並抱怨變量kv 不存在。這是因為模棱兩可:unquote(k) 可以像以前一樣是取消引用的片段,也可以是像 unquote(kv) 中的常規取消引用。

此問題的一種解決方案是禁用宏中的取消引用,但是,這樣做會導致無法將 kv 表示形式注入到樹中。這就是:bind_quoted 選項來救援的時候(再次!)。通過使用 :bind_quoted ,我們可以自動禁用取消引用,同時仍將所需的變量注入到樹中:

defmacro defkv(kv) do
  quote bind_quoted: [kv: kv] do
    Enum.each(kv, fn {k, v} ->
      def unquote(k)(), do: unquote(v)
    end)
  end
end

事實上,每次想要在引號中注入一個值時,都建議使用 :bind_quoted 選項。

相關用法


注:本文由純淨天空篩選整理自elixir-lang.org大神的英文原創作品 Kernel.SpecialForms.quote(opts, block)。非經特殊聲明,原始代碼版權歸原作者所有,本譯文未經允許或授權,請勿轉載或複製。