[elixir! #0005] [譯] 理解Elixir中的宏——part.3 深刻AST by Saša Jurić

是時候繼續探索Elixir中的宏了。上一次我講了一些微觀理論,今天,我將會進入一個較少提到的領域,並討論Elixir AST的一些細節。html

跟蹤函數調用

目前爲止,你只見到了基礎的宏,它們獲得AST片斷,而後將其結合起來,周圍加上一些模板。咱們沒有分析或解析輸入的AST,因此這多是最乾淨(或者說最少hack技巧)的編寫宏的方法,獲得的會是容易理解的簡單的宏。git

而後,有時咱們須要解析輸入的AST片斷以獲取某些信息。一個簡單的例子是ExUnit的斷言。例如,表達式assert 1+1 == 2+2會出現這個錯誤:github

Assertion with == failed
code: 1+1 == 2+2
lhs:  1
rhs:  2

assert接收了整個表達式1+1 == 2+2,而後從中分出獨立的表達式用來比較,若是整個表達式返回false,則打印它們對應的結果。因此,宏的代碼必須想辦法將輸入的AST分解爲幾個部分並分別計算子表達式。shell

更多時候,咱們調用了更復雜的AST變換。例如, 你能夠藉助ExActor這樣寫:less

defcast inc(x), state: state, do: new_state(state + x)

它會被轉換成差很少這樣:函數

def inc(pid, x) do
  :gen_server.cast(pid, {:inc, x})
end

def handle_cast({:inc, x}, state) do
  {:noreply, state+x}
end

assert同樣,宏defcast須要深刻輸入的AST片斷,並找出每一個子片斷(例如,函數名,每一個參數)。而後,ExActor執行一個精巧的變換,將各個部分重組成一個更加複雜的代碼。優化

今天,我將想你展現構建這類宏的基礎技術,我也會在以後的文章中將變換作得更復雜。但在此以前,我要請你認真考慮一下:你的代碼是否有有必要基於宏。儘管宏十分強大,但也有缺點。ui

首先,就像以前咱們看到的那樣,比起那些「純」的運行時抽象,宏的代碼會很快地變得很是多。你能夠依賴沒有文檔格式的AST來快速完成許多嵌套的quote/unquoted調用,以及奇怪的模式匹配。編碼

此外,宏的濫用可能使你的客戶端代碼極其難懂,由於它將依賴於自定義的非標準習語(例如ExActor的defcast)。這使得理解代碼和了解底層究竟發生了什麼變得更加困難。code

反過來,宏在刪除模板方面很是管用(例如ExActor的例子所演示的),並且宏有權訪問那些在運行時不可用的信息(正如你應該從assert例子中看到的)。最後,因爲它們在編譯期間運行,宏能夠經過將計算移動到編譯時來優化一些代碼。

所以,確定會有適合使用宏的情形,你沒必要懼怕使用它們。但你不該該只是爲了獲取一些可愛的DSL語法而使用宏。在考慮宏以前,你應該先考慮你的問題是否能夠依賴於「標準」的語言抽象,例如函數,模塊和協議,在運行時有效解決。

探索AST結構

目前,關於AST結構的文檔很少。然而,在shell會話中能夠很簡單地探索和使用AST,我一般就是這樣探索AST格式的。

例如,這是一個被引用了的變量:

iex(1)> quote do my_var end
{:my_var, [], Elixir}

這裏,第一個元素表明變量的名稱。第二個元素是上下文關鍵字列表,它包含了該AST片斷的元數據(例如導入和別名)。一般你不會對上下文數據感興趣。第三個元素一般表明引用發生的模塊,同時也用於確保引用變量的衛生。若是該元素爲nil,則該標識符是不衛生的。

一個簡單的表達式看起來包含了許多:

iex(2)> quote do a+b end
{:+, [context: Elixir, import: Kernel], [{:a, [], Elixir}, {:b, [], Elixir}]}

看起來可能很可怕,可是若是展現更高層次的模式,就很容易理解了:

{:+, context, [ast_for_a, ast_for_b]}

在咱們的例子中,ast_for_aast_for_b遵循着你以前所看到的變量的形狀(如{:a, [], Elixir})。通常,引用的參數能夠是任意複雜的,由於它們描述了每一個參數的表達式。事實上,AST是一個簡單引用的表達式的深層結構,就像我給你展現的這樣。

讓咱們來看一個函數調用:

iex(3)> quote do div(5,4) end
{:div, [context: Elixir, import: Kernel], [5, 4]}

這相似於引用+操做,咱們知道+其實是一個函數。事實上,全部二進制運算符都會像函數調用同樣被引用。

最後,讓咱們來看一個引用了的函數定義:

iex(4)> quote do def my_fun(arg1, arg2), do: :ok end
{:def, [context: Elixir, import: Kernel],
 [{:my_fun, [context: Elixir], [{:arg1, [], Elixir}, {:arg2, [], Elixir}]},
  [do: :ok]]}

看起來有點嚇人,但能夠經過只看重要的部分來簡化它。事實上,這種深層結構至關於:

{:def, context, [fun_call, [do: body]]}

fun_call是一個函數調用的結構(你看過的那樣)。

如你所見,AST背後一般有一些緣由和意義。我不會在這裏寫出全部AST的形狀,但會在iex中嘗試你感興趣的簡單的格式來探索AST。這是一個反向工程,但不是火箭科學。

寫斷言宏

爲了快速演示,讓咱們來編寫一個簡化版本的assert宏。這是一個有趣的宏,由於它從字面上從新解釋了比較運算符的意義。一般,當你寫a == b時,你會獲得一個布爾值。但將此表達式賦給assert宏時,若是表達式計算結果爲false,就會輸出詳細的結果。

我將從簡單的部分開始,首先在宏裏只支持==運算符。想一下,當咱們調用assert expected == required,等同於調用assert(expect == required),這意味着咱們的宏接收到一個表示比較的引用片斷。讓咱們來探索這個比較的AST結構:

iex(1)> quote do 1 == 2 end
{:==, [context: Elixir, import: Kernel], [1, 2]}

iex(2)> quote do a == b end
{:==, [context: Elixir, import: Kernel], [{:a, [], Elixir}, {:b, [], Elixir}]}

因此咱們的結構本質上是{:==, context, [quoted_lhs, quoted_rhs]}。若是你記住了前面章節中所示的例子,那麼就不會感到意外,由於我提到過二進制運算符是做爲二參數函數被引用。

知道了AST的形狀,編寫宏會相對簡單:

defmodule Assertions do
  defmacro assert({:==, _, [lhs, rhs]} = expr) do
    quote do
      left = unquote(lhs)
      right = unquote(rhs)

      result = (left == right)

      unless result do
        IO.puts "Assertion with == failed"
        IO.puts "code: #{unquote(Macro.to_string(expr))}"
        IO.puts "lhs: #{left}"
        IO.puts "rhs: #{right}"
      end

      result
    end
  end
end

第一件有趣的事發生在第二行。注意咱們是如何模式匹配輸入表達式的,指望它去符合一些結構。這是徹底正常的,由於宏也是函數,因此你能夠依賴模式匹配,guard語句,甚至是多個從句的宏。在咱們的例子中,咱們依靠模式匹配將比較表達式的每一個(引用的)一側轉換爲相應的變量。

而後,在引用的代碼中,咱們從新解釋了==操做,經過分別計算左側和右側(第4行和第5行),以及整個的結果(第7行)。最後,若是結果是false,咱們將打印詳細信息(第9到14行)。

來試一下:

iex(1)> defmodule Assertions do ... end
iex(2)> import Assertions

iex(3)> assert 1+1 == 2+2
Assertion with == failed
code: 1 + 1 == 2 + 2
lhs: 2
rhs: 4

代碼通用化

使代碼適用於其它運算符並不難:

defmodule Assertions do
  defmacro assert({operator, _, [lhs, rhs]} = expr)
    when operator in [:==, :<, :>, :<=, :>=, :===, :=~, :!==, :!=, :in]
  do
    quote do
      left = unquote(lhs)
      right = unquote(rhs)

      result = unquote(operator)(left, right)

      unless result do
        IO.puts "Assertion with #{unquote(operator)} failed"
        IO.puts "code: #{unquote(Macro.to_string(expr))}"
        IO.puts "lhs: #{left}"
        IO.puts "rhs: #{right}"
      end

      result
    end
  end
end

這裏只有一點點變化。首先,在模式匹配中,硬編碼:==被變量operator取代了(第2行)。

我還引入(實際上,是從Elixir源代碼中複製粘貼了)guard語句指定了宏能處理的運算符集(第3行)。這個檢查有一個特殊緣由。還記得我以前提到的,引用a + b(或任何其它的二進制操做)的形狀等同於引用fun(a, b)。所以,沒有這些guard語句,任何雙參數的函數調用都會在咱們的宏中結束,這多是咱們不想要的。使用這個guard語句能將輸入限制在已知的二進制運算符中。

有趣的事情發生在第9行。在這裏我使用了unquote(operator)(left, right)來對操做符進行簡單的泛型分派。你可能認爲我可使用left unquote(operator) right來替代,但它並不能運做。緣由是operator變量保存的是一個原子(如:==)。所以,這個天真的引用會產生left :== right,這甚至不符合Elixir語法。

記住,在引用時,咱們不組裝字符串,而是AST片斷。因此,當咱們想生成一個二進制操做代碼時,咱們須要注入一個正確的AST,它(如前所述)與雙參數的函數調用相同。所以,咱們能夠簡單地生成函數調用unquote(operator)(left, right)

這一點講完了,今天的這一章也該結束了。它有點短,但略微複雜些。下一章,我將深刻AST解析的話題。

Copyright 2014, Saša Jurić. This article is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.
The article was first published on the old version of the Erlangelist site.
The source of the article can be found here.

相關文章
相關標籤/搜索