是時候繼續探索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結構的文檔很少。然而,在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_a
和ast_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.