檢視原始碼 引號和取消引號

本指南旨在介紹 Elixir 中可用的元程式設計技巧。透過其自身資料結構表示 Elixir 程式,是元程式設計的核心。本章節從探討這些結構和相關的 quote/2unquote/1 建構開始,以便我們在下一指南中檢視巨集,最後建立我們自己的特定領域語言。

引號

Elixir 程式的建構區塊是包含三個元素的元組。例如,函式呼叫 sum(1, 2, 3) 在內部表示為

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

你可以透過使用 quote/2 巨集取得任何表達式的表示

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

第一個元素是函式名稱,第二個是包含元資料的關鍵字清單,第三個是引數清單。

運算子也表示為此類元組

iex> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}

甚至地圖也表示為對 %{} 的呼叫

iex> quote do: %{1 => 2}
{:%{}, [], [{1, 2}]}

變數使用此類三重項表示,不同之處在於最後一個元素是原子,而不是清單

iex> quote do: x
{:x, [], Elixir}

在引號更複雜的表達式時,我們可以看到程式以此類元組表示,這些元組通常在類似樹狀結構中彼此巢狀。許多語言會將此類表示稱為 抽象語法樹 (AST)。Elixir 稱它們為引號表達式

iex> quote do: sum(1, 2 + 3, 4)
{:sum, [], [1, {:+, [context: Elixir, import: Kernel], [2, 3]}, 4]}

有時,在使用引號表達式時,取得文字程式表示可能會很有用。這可以使用 Macro.to_string/1 來完成

iex> Macro.to_string(quote do: sum(1, 2 + 3, 4))
"sum(1, 2 + 3, 4)"

一般而言,上述元組會根據下列格式進行結構化

{atom | tuple, list, list | atom}
  • 第一個元素是原子或其他採用相同表示的元組;
  • 第二個元素是包含元資料的關鍵字清單,例如數字和內容;
  • 第三個元素可以是函數呼叫參數清單或原子。當此元素為原子時,表示元組代表變數。

除了上述定義的元組外,還有五個 Elixir 文字,在引用時會回傳它們自己(而不是元組)。它們是

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

大多數 Elixir 程式碼都可以直接轉換成其底層引用表達式。我們建議您嘗試不同的程式碼範例,看看結果如何。例如,String.upcase("foo") 會擴充成什麼?我們也學到 if(true, do: :this, else: :that) 等於 if true do :this else :that end。此肯定句在引用表達式中如何成立?

取消引用

引用是關於擷取特定程式碼區塊的內部表示法。然而,有時可能需要在我們想要擷取的表示法中注入其他特定程式碼區塊。

例如,假設您有一個名為 number 的變數,其中包含您想要注入引用表達式中的數字。

iex> number = 13
iex> Macro.to_string(quote do: 11 + number)
"11 + number"

這不是我們想要的,因為 number 變數的值尚未注入,而且 number 已在表達式中被引用。為了注入 number 變數的,必須在引用表示法中使用 unquote/1

iex> number = 13
iex> Macro.to_string(quote do: 11 + unquote(number))
"11 + 13"

unquote/1 甚至可以用來注入函數名稱

iex> fun = :hello
iex> Macro.to_string(quote do: unquote(fun)(:world))
"hello(:world)"

在某些情況下,可能需要在清單中注入多個值。例如,假設您有一個包含 [1, 2, 6] 的清單,我們想要注入 [3, 4, 5]。使用 unquote/1 無法產生預期的結果

iex> inner = [3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote(inner), 6])
"[1, 2, [3, 4, 5], 6]"

此時,unquote_splicing/1 就派上用場了

iex> inner = [3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote_splicing(inner), 6])
"[1, 2, 3, 4, 5, 6]"

在使用巨集時,取消引用非常有用。在撰寫巨集時,開發人員可以接收程式碼區塊,並將它們注入其他程式碼區塊,可用於轉換程式碼或撰寫在編譯期間產生程式碼的程式碼。

跳脫

正如我們在本章節一開始所看到的,只有某些值是 Elixir 中有效的引用表達式。例如,映射不是有效的引用表達式。四個元素的元組也不是。然而,這些值可以表示為引用表達式

iex> quote do: %{1 => 2}
{:%{}, [], [{1, 2}]}

在某些情況下,您可能需要將這些注入引用表達式中。為此,我們需要先使用 Macro.escape/1 將這些值跳脫為引用表達式

iex> map = %{hello: :world}
iex> Macro.escape(map)
{:%{}, [], [hello: :world]}

巨集接收引號表達式,且必須傳回引號表達式。然而,有時在巨集執行期間,您可能需要使用值,且必須區分值與引號表達式。

換句話說,區分一般 Elixir 值(例如清單、映射、程序、參考等)與引號表達式非常重要。某些值,例如整數、原子和字串,其引號表達式等於值本身。其他值,例如映射,則需要明確轉換。最後,函數和參考等值根本無法轉換為引號表達式。

使用巨集和產生程式碼的程式碼時,請查看 Macro 模組的說明文件,其中包含許多用於處理 Elixir AST 的函數。

在本簡介中,我們已奠定基礎,終於可以撰寫我們的首個巨集。您可以在 下一指南 中查看。