elixir官方教程 元編程(二) 宏

#宏express

  1. 前言
  2. 咱們的第一個宏
  3. 宏的隔離
  4. 環境
  5. 私有宏
  6. 負責任地編寫宏

#前言安全

儘管Elixir已竭力爲宏提供一個安全的環境,用宏編寫乾淨代碼的責任仍然落在了開發者身上.宏比傳統的Elixir函數更難編寫,並且在沒必要要的場合使用宏是很差的.因此請負責任地編寫宏.數據結構

Elixir已經提供了許多數據結構和函數,可以讓你以簡單可讀的風格編寫平常代碼.宏應當是最後的選擇.記住,明顯賽過含蓄.清晰的代碼賽過簡潔的代碼.less

#咱們的第一個宏函數

ELixir中使用defmacro/2來定義宏.測試

本章,咱們將使用文件來代替在IEx中運行樣本代碼.這是由於代碼樣本將跨越許多行,將它們所有輸入IEx會拔苗助長.你應當將代碼樣本保存進macro.exs文件,並使用elixir macros.exsiex macro.exs來運行.ui

爲了更好地理解宏是如何運做的,讓咱們建立一個新的模塊,在其中實現unless,它的做用與if相反.分別以函數和宏的形式:this

defmodule Unless do
  def fun_unless(clause, expression) do
    if(!clause, do: expression)
  end

  defmacro macro_unless(clause, expression) do
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end
end

函數接收了參數,並傳送給if.然而正如咱們在前一章所學過的,宏會接收引用表達式,將它們注入引用,最後返回另外一個引用表達式.設計

讓咱們用iex運行上面的模塊:code

$ iex macros.exs

調戲一下那些定義:

iex> require Unless
iex> Unless.macro_unless true, IO.puts "this should never be printed"
nil
iex> Unless.fun_unless true, IO.puts "this should never be printed"
"this should never be printed"
nil

注意,在宏的實現中,句子沒有被打印,然而在函數的實現中,句子被打印了.這是由於函數的參數會在調用函數以前被執行.而宏不會執行它們的參數.它們以引用表達式的形式接收參數,以後又將其變形爲其它引用表達式.本例中,咱們其實是將unless宏重寫成了一個if.

換句話說,當被這樣調用時:

Unless.macro_unless true, IO.puts "this should never be printed"

咱們的macro_unless宏接收到了:

macro_unless(true, {{:., [], [{:aliases, [], [:IO]}, :puts]}, [], ["this should never be printed"]})

而後返回了一個引用表達式:

{:if, [],
 [{:!, [], [true]},
  [do: {{:., [],
     [{:__aliases__,
       [], [:IO]},
      :puts]}, [], ["this should never be printed"]}]]}

咱們可使用Macro.expand_once/2來驗證它:

iex> expr = quote do: Unless.macro_unless(true, IO.puts "this should never be printed")
iex> res  = Macro.expand_once(expr, __ENV__)
iex> IO.puts Macro.to_string(res)
if(!true) do
  IO.puts("this should never be printed")
end
:ok

Macro.expand_once/2接收了引用表達式,並根據當前環境擴展了它.本例中,它擴展/調用了Unless.macro_unless/2宏,並返回告終果.以後咱們將返回的引用表達式轉換成一個字符串並打印出來(咱們將在本章稍後的位置討論__ENV__).

這就是宏.它們接收引用表達式並將其變形爲別的東西.事實上,Elixir中的unless/2是做爲宏來實現的:

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

本教程中用到的許多純Elixir實現的結構都是宏,例如unless/2,defmacro/2,def/2,defprotocol/2等等.這意味着,開發者能夠用構建語言的結構來將語言擴展到它們工做的領域.

咱們能夠定義任何函數和宏,甚至覆蓋Elixir中的本來定義.惟一的例外是Elixir特殊形式,它們不是由Elixir實現的,所以不能被覆蓋,特殊形式的完整列表能夠在Kernel.SpecialForms中找到.

#宏的隔離(Macros hygiene)

Elixir的宏有着低決定權.這保證了引用中的變量定義不會與宏被擴展到的語境中的變量定義相沖突.例如:

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,卻沒有影響到變量a在函數go中的定義.若是宏想要明確地影響語境,可使用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使用變量的語境來註解它,因此可以實現變量隔離.例如,一個模塊的第三行定義的變量x能夠被表示成:

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

然而一個引用變量是這樣表示的:

defmodule Sample do
  def quoted do
    quote do: x
  end
end

Sample.quoted #=> {:x, [line: 3], Sample}

注意引用變量的第三個元素是原子Sample,而不是nil,它標記了變量是來自Sample模塊的.所以,Elixir認爲這兩個變量來自不一樣語境,會分別處理它們.

Elixir也爲進口(imports)和別名(aliases)提供了類似的機制.這保證了宏的行爲會與它源模塊中的定義相同,而不是與宏所擴展到的目標模塊相沖突.使用相似var!/2alias!/2之類的宏能夠突破隔離,可是它們必須當心使用,由於這直接改變了用戶環境.

有時,變量名會被動態地建立.Macro.var/2可用於定義新變量:

defmodule Sample do
  defmacro initialize_to_char_count(variables) do
    Enum.map variables, fn(name) ->
      var = Macro.var(name, nil)
      length = name |> Atom.to_string |> String.length
      quote do
        unquote(var) = unquote(length)
      end
    end
  end

  def run do
    initialize_to_char_count [:red, :green, :yellow]
    [red, green, yellow]
  end
end

> Sample.run #=> [3, 5, 6]

注意Macro.var/2的第二個變量.在下一節中咱們將知道它是所使用的語境,並且能定義隔離.

#環境

本章早些時候,咱們調用Macro.expand_once/2時,使用了特殊形式__ENV__.

__ENV__返回了一個Macro.Env結構的實例,它包含了編譯環境的有用信息,包括當前模塊,文件和行,全部定義在當前做用域中的變量,還有imports,requires等等.

iex> __ENV__.module
nil
iex> __ENV__.file
"iex"
iex> __ENV__.requires
[IEx.Helpers, Kernel, Kernel.Typespec]
iex> require Integer
nil
iex> __ENV__.requires
[IEx.Helpers, Integer, Kernel, Kernel.Typespec]

Macro模塊中的許多函數都指望一個環境.你能夠在Macro模塊中找到關於這些函數,以及在Macro.Env的文檔中找到關於編譯環境的更多信息.

#私有宏

Elixir也支持私有宏,使用defmacrop來定義.和私有函數同樣,這些宏只能在它的定義模塊中使用,並且只在編譯時.

很重要的一點是,宏在使用以前定義.沒有在調用一個宏以前定義它,將會在運行時拋出一個錯誤,由於宏不會被擴展,並且將會被轉化成函數調用:

iex> defmodule Sample do
...>  def four, do: two + two
...>  defmacrop two, do: 2
...> end
** (CompileError) iex:2: function two/0 undefined

#負責任地編寫宏

宏是很強大的結構,Elixir提供了許多機制來確保它們被負責任地使用.

- 宏是隔離的: 定義在宏內的變量默認是不會影響用戶代碼的.並且,宏語境中的函數調用和別名是不會泄露到用戶語境中的.

- 宏具備詞典性質: 不可能全局地注入代碼或宏.爲了使用宏,你須要明確地requireimport定義了宏的模塊.

- 宏是明確的: 宏不可能在沒有明確被導入的狀況下運行.例如,一些語言容許開發者在內部徹底重寫函數,一般是經過語義轉換或一些反射機制.在Elixir中,編譯時,宏必須在調用者中被明確導入.

- 宏的語言是清晰的: 許多語言爲quoteunquote提供了語法捷徑.在Elixir中,咱們更願意它們被明確地拼寫出來,以便清楚地劃出宏定義與它的引用表達式間的界限.

即便有這些保障,開發者仍在負責任地編寫宏這件事中扮演重要角色.若是你確信你須要使用宏,記住宏不是你的API.你的宏定義要保持簡短,包括它們的引用內容.例如,與其像這樣編寫宏:

defmodule MyModule do
  defmacro my_macro(a, b, c) do
    quote do
      do_this(unquote(a))
      ...
      do_that(unquote(b))
      ...
      and_that(unquote(c))
    end
  end
end

不如這樣:

defmodule MyModule do
  defmacro my_macro(a, b, c) do
    quote do
      # Keep what you need to do here to a minimum
      # and move everything else to a function
      do_this_that_and_that(unquote(a), unquote(b), unquote(c))
    end
  end

  def do_this_that_and_that(a, b, c) do
    do_this(a)
    ...
    do_that(b)
    ...
    and_that(c)
  end
end

這使得你的代碼更清晰,也更容易測試和維護,由於你能夠直接調用和測試do_this_that_and_that/3.這也有助於你爲那些不肯意依賴宏的開發者來設計一個實際的API.

如今,咱們結束了對宏的介紹.下一章咱們將簡短得討論DSL,展現如何混合宏和模塊屬性,來註釋和擴展模塊與函數.

相關文章
相關標籤/搜索