5. Macros

5 宏(Macros)


一段Elixir程序可以展現(xiàn)為一組數(shù)據(jù)結(jié)構(gòu)。本章將說明這些結(jié)構(gòu)(看起來)是怎樣的,以及如何使用它們來創(chuàng)建你自己的宏。

5.1 Elixir程序的建構(gòu)block(building block)


Elixir程序的建構(gòu)block是一個包含三個元素的元組。例如,名為 sum(1,2,3) 的函數(shù)在 Elixir 中可以被展現(xiàn)為這樣:

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

你可以使用 quote 這個宏來獲取任意表達(dá)式的展現(xiàn)方式:

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

操作符同樣能被展現(xiàn)為這樣的元組:

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

甚至一個元組也可以表現(xiàn)為對 {} 的一次調(diào)用:

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

變量也能用類似元組表現(xiàn),只是最后一個元組不再是一個list,而是atom原子:

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

當(dāng)我們引用(譯注:quoting,指用 quote 宏來展現(xiàn)一個表達(dá)式)更復(fù)雜的表達(dá)式時,可以看到其展現(xiàn)方式是元組組合而成,其中的元組彼此嵌套為相似的樹,其中的每個節(jié)點都是元組。

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

通常,上述的每個節(jié)點(即元組)會遵循下列格式:

{ tuple | atom, list, list | atom }

元組的第一個元素是是一個atom原子或類似的展現(xiàn)元組

元組的第二個元素是一個元數(shù)據(jù)list,它將存放類似節(jié)點行號的元信息

元組的第三個元素可能是一個參數(shù)列表或者一個atom原子,如果是后者,表示這個元組是展現(xiàn)一個變量

除了上面定義的節(jié)點,有5種Elixir字面量會在引用(解釋同上)時返回其本身(不是元組)。他們是:

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

掌握了這些基本概念后,我們就可以準(zhǔn)備定義自己的宏了。


5.2 定義我們自己的宏


可以使用 defmacro 定義一個宏。例如,通過下面少量幾行代碼,我就可以定義一個名叫 unless 的宏,讓它起到和 if 相反的效果:

defmodule MyMacro do
  defmacro unless(clause, options) do
    quote do: if(!unquote(clause), unquote(options))
  end
end

類似 if , unless 需要兩個參數(shù)—— clause 和 options :

require MyMacro
MyMacro.unless var, do: IO.puts "false"

這樣,既然 unless 是一個宏,它的參數(shù)就不應(yīng)當(dāng)在(unless)被調(diào)用時求值,而是應(yīng)該直接傳入字面量。比如,如果有一個這樣的調(diào)用

unless 2 + 2 == 5, do: call_function()

我們的 unless 宏將會接受到下面的信息:

unless({:==, [], [{:+, [], [2, 2]}, 5]}, { :call_function, [], [] })

那么 unless 宏就要調(diào)用 quote 來返回一個 if 語句的結(jié)構(gòu)樹,這意味著我們將 unless 轉(zhuǎn)換為 if!

引用表達(dá)式的時候一個常見的錯誤是開發(fā)者常常會忘記 unquote 那個表達(dá)式。為了理解 unquote 所做的事情,讓我們簡單的去除它看看結(jié)果:

defmacro unless(clause, options) do
  quote do: if(!clause, options)
end

當(dāng)我們調(diào)用unless 2 + 2 == 5, do: call_function(), 我們的unless將返回字面量

if(!clause, options)

由于clause和options這兩個變量沒有定義在當(dāng)前上下文中,執(zhí)行就會失敗。而如果我們把unquote添加回來:

defmacro unless(clause, options) do
  quote do: if(!unquote(clause), unquote(options))
end

unless 將會返回:

if(!(2 + 2 == 5), do: call_function())

換句話說,unquote 是一個將表達(dá)式注入到被引用的解析樹的機(jī)制,同時也是元編程的核心工具。Elixir同時還提供 unquote_splicing 來允許我們一次注入多個表達(dá)式

我們可以定義我們需要的任何宏——甚至可以覆蓋掉Elixir內(nèi)建的宏。例如,你可以重新定義 case , receive , + ... 等等。但是 Elixir 有一些特殊形式不能被覆蓋,在 Kernel.SpecialForms 中有這些形式的完整列表。

5.3 宏的安全性


Elixir宏會被延遲解析,這將保證在展開宏時,定義在quote中的變量不會與上下文中的變量定義沖突,例如:

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

defmodule HygieneTest do def go do require Hygiene a = 13 Hygiene.no_interference a end end HygieneTest.go # => 13

在上述例子中,即使在宏中注入 a = 1 ,那也不會影響到定義在 go 函數(shù)中的a變量。在某些場景中,宏需要顯式的影響上下文(的變量),我們可以使用 var!:

defmodule Hygiene do
  defmacro interference do
    quote do: var!(a) = 1
  end
end
defmodule HygieneTest do
  def go do
    require Hygiene
    a = 13
    Hygiene.interference
    a
  end
end
HygieneTest.go
# => 1

安全的變量僅僅由于Elixir在相應(yīng)上下文中標(biāo)注了變量而正常工作。例如,變量 x 定義在模塊的第三行,展開以后就是這樣:

{ :x, [line: 3], nil }

而一個被引用的變量(代碼中未定義)展開以后就會是這樣:

defmodule Sample do
  def quoted do
    quote do: x
  end
end
Sample.quoted #=> { :x, [line: 3], Sample }

注意:在引用變量的展現(xiàn)方式里的第三個元素是一個原子 Sample ,而不是 nil ,這標(biāo)記了這個變量來自 Sample 這個module。這樣,Elixir就能根據(jù)這些信息正確處理這兩個來自不同上下文的變量。

Elixir為imports和aliases提供了相似的機(jī)制,以確保宏將與其所在的特定源碼行為一致而不是與目標(biāo)模塊沖突。

5.4 私有宏


Elixir使用 defmacrop 支持私有宏。這些宏將僅能在所定義的模塊內(nèi)部被使用,就像私有函數(shù),只不過它是在編譯時工作的。一個常見的關(guān)于私有宏的例子是定義在同一個模塊內(nèi)部被頻繁使用的 guard :

defmodule MyMacros do
  defmacrop is_even?(x) do
    quote do
      rem(unquote(x), 2) == 0
    end
  end
  def add_even(a, b) when is_even?(a) and is_even?(b) do
    a + b
  end
end

很重要的一點是:宏必須在使用之前定義。如果沒有在調(diào)用前定義宏,那么我們將收到一個運行時錯誤,因為此時宏無法被展開并轉(zhuǎn)換為函數(shù)調(diào)用:

defmodule MyMacros do
  def four, do: two + two
  defmacrop two, do: 2
end
MyMacros.four #=> ** (UndefinedFunctionError) undefined function: two/0


5.5 代碼執(zhí)行


在結(jié)束關(guān)于宏的討論之前,我們將簡短的論述代碼是如何在Elixir中被執(zhí)行的。在Elixir中,代碼的完整執(zhí)行涉及兩個步驟:

1) 代碼中的所有宏將被遞歸的展開;

2) 被展開的代碼將被編譯為Erlang字節(jié)碼并被執(zhí)行

理解這些非常重要,因為這會影響我們?nèi)绾慰创覀兊拇a結(jié)構(gòu)。看看如下的代碼:

defmodule Sample do
  case System.get_env("FULL") do
    "true" ->
      def full?(), do: true
    _ ->
      def full?(), do: false
  end
end

上述代碼將定義一個名為 full? 的函數(shù),它將根據(jù)編譯時的環(huán)境變量 FULL 的值返回 true 或者 false。為了執(zhí)行這段代碼,Elixir將首先展開所有的宏。而因為 defmodule 和 def 本身也是宏,代碼將被展開為類似下面的樣子:

:elixir_module.store Sample, fn ->
  case System.get_env("FULL") do
    "true" ->
      :elixir_def.store(Foo, :def, :full?, [], true)
    _ ->
      :elixir_def.store(Foo, :def, :full?, [], false)
end


接著,代碼將被執(zhí)行,定義一個名為 Foo的模塊,并在這個模塊內(nèi)部存放一個關(guān)聯(lián)的函數(shù),這個函數(shù)基于環(huán)境變量 FULL 的值。達(dá)成這些需要使用 elixir_module 和 elixir_def 函數(shù),這兩個函數(shù)都是來自Elixir內(nèi)部模塊,本身使用erlang編寫。

這個例子中我們可以學(xué)到兩點:

1) 宏總是會被展開的,無論它所在 case 的分支是否會被執(zhí)行到;

2) 我們不能緊接著一個函數(shù)或者宏的定義之后來調(diào)用它,例如如下代碼:

defmodule Sample do
  def full?, do: true
  IO.puts full?
end

這段代碼將會失敗,因為它會被轉(zhuǎn)換成這樣:

:elixir_module.store Sample, fn ->
  :elixir_def.store(Foo, :def, :full?, [], true)
  IO.puts full?
end

此時,模塊正在被定義,(因而)還沒有一個名為 full? 的函數(shù)被定義在模塊中,這樣, IO.puts full? 調(diào)用就會遇到編譯失敗。

5.6 避免使用宏Don't write macros


考慮到宏是一個很強(qiáng)大的編程結(jié)構(gòu),在這個領(lǐng)域的第一條原則是——避免使用宏。相比于普通的Elixir函數(shù),宏的編寫是比較難的,在不必要的時候使用宏被認(rèn)為是一個不好的風(fēng)格(bad style)。Elixir已經(jīng)提供了很多優(yōu)雅的機(jī)制幫助你日常的編碼工作,宏應(yīng)當(dāng)被作為最后手段。

通過上述課程,我們結(jié)束了對宏的介紹。接下來,讓我們進(jìn)入下一章,討論代碼文檔、非完整應(yīng)用(partial application)和一些其它話題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容