iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 28
1

在討論 Phoenix 的 web.ex 之前,我們先來聊聊 Elixir 中我最喜歡的特性,macro。熟悉 Ruby 的人,可能會知道 Ruby 帶有許多好用的 meta-programming 功能,可以把程式做為資料,在執行期操作,像是 method_missinginstance_eval 等。

Macro 常被描述成是 Elixir 的 mata programming 功能,但其實它遠不止於此。 Elixir 語言本身的大部份,就是靠 macro 這個機制建構起來的。你所使用的 defif+ 等等,說穿了都只是個 macro 而己。我自己喜歡這樣想:作者 José Valim 在重新設計* Elixir 這個語言時,最先打造了 macro 這個抽象工具,並用它完成了這語言剩下大部份的功能。但他沒有把這個工具藏起來,而是把這工具也交給 programmer,讓你也能有改造語言本身的能力。

但在開始之前,就像所有的 meta programming 教學一樣,總是要帶個警語的。想要在 production 環境裡使用這些高階功能,請務必先默念一下蜘蛛人法則,也就是能力愈強,責任也愈大。具體來說,就是兩條規則:

  1. 若非不用 macro 就無法實作的功能,就請儘量避免。
  2. 一定要用 macro 時,請仔細思索各種可能發生的情況,並留下足夠的除錯訊息,否則若事情出了問題,會非常難發現及處理。

所以說把什麼東西都做成其實很難用很難學的 internal DSL 顯然不是個好方向。但是亂搞是非常有趣的,從亂搞之中我們也可以學到許多很棒的知識。正如我景仰的 Gea-Suan Lin 所說:幹壞事是進步最大的原動力。

Note *: José 在幾次演講中提過,Elixir 自 2013 開始開發時並沒有規劃得很好,在進行了幾個月後遇到各種重大困難就停滯了。而在停了約半年之後他們重新思索整體的架構,把整個專案打掉重新來過。

把 expression 解開

你有沒有想過你寫在程式檔案裡的東西,例如說 1 + 2 ,是怎麼跑起來的?把 iex 打開,我們這樣來試試看:

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

quote/2 是 macro 中的核心語法。它會將一個 Elixir 的表達式,拆解成一個 3-tuple,第一個是用 atom 表示的運算子,也就是函式名稱,第二個參數是個 keyword list,裡面放了環境參數,而第三個也是一個串列,代表這個函式呼叫所帶上的參數。

那如果是長一點的式子呢:

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

在第三個串列,參數列表裡的第二個元素,現在也是另一個 3-tuple 了。當表達式愈複雜, 每個 3-tuple 的參數都可能包含另一個 3-tuple,這模式會一直延展下去。

當我們在文字檔案裡寫下程式碼,一直到進入 VM 執行之前,發生了四件事。首先把一個個的字元切成對這個語言有意義的元素,例如 if3end 等等,這一步叫 tokenize。接著將一個個的 token 依語法規則組成有意義的執行結構,這一步叫 parse,所產出的結果會是一張樹狀圖,叫做 Abstract Syntax Tree (AST、抽象語法樹)。再接著依 AST 產生 VM 用的執行指令,這一步叫 compile

而 Elixir 的 macro,就是將解析完成的抽象語法樹用 3-tuple 的型式交還給你,讓你可以直接動手亂改。當你把改好的 3-tuple 遞回去,他就會幫你執行這份新的結構。我們這樣試試看:

iex> ast = {:-, [], [20, 10]} #先做一個語法樹
iex> Code.eval_quoted(ast)
{10, []}

寫一個更厲害的函式

除了手動處理 3-tuple 之外,Elixir 還提供了 defmacro 來操作 AST。我們先新增一個檔案叫 bad_math.exs,並輸入以下的內容:

defmodule BadMath do
  defmacro calc({_op, _, [x, y]}) when is_integer(x) and is_integer(y) do
    {:+, [], [x, y]}
  end
end

BadMath.calc/1 會幫我們把運算式解開,但不管是哪一種四則運算,我們都把它改成 +/2

在檔案所在的目錄下打開 iex。由於 macro 需要進行編譯及引用才能操作,所以我們要先輸入兩個指令:

iex> c "bad_math.exs"
[BadMath]
iex> require BadMath
BadMath

接著就可以來操作看看了:

iex> BadMath.calc 100 - 99
199

不用自己組 AST

上面的例子雖然很酷,但是要每次手動組 AST 的 3-tuple 實在太累了。有沒有什麼語法可以把表達式直接轉成 AST 呢?等等,我們一開始不就是用了 quote/2 來做這件事嗎?

defmacro calc({_op, _, [x, y]}) when is_integer(x) and is_integer(y) do
  quote do: x + y
end

但在繼續之前請先停一下,因為這樣是不能動的。這個 quote 語法實際上會產生 {:+, [], [x, y]},而發現找不到 xy 這兩個變數。我們需要有個方式在 quote/2 裡跟它說這邊的變數是需要替換成值的,這就是 unquote/1 的功能了:

defmacro calc({_op, _, [x, y]}) when is_integer(x) and is_integer(y) do
  quote do: unquote(x) + unquote(y)
end

回到 iex,我們要重新載入改好的模組:

iex> r BadMath
{:reloaded, BadMath, [BadMath]}
iex> BadMath.calc 100 - 99
199

還有更多

Elixir 中的 macro 是個很大的題目,限於篇幅我們就在這裡停步了。如果你也很喜歡這個部份,可以去看 Pragmatic Bookshelf 出的 Metaprogramming Elixir ,本書作者就是 Phoenix 的作者 Chris McCord,內容非常有啟發性。

另外我自己做的 Elixir 套件 pipe_to,可以讓你把參數 pipe 到其它的位置。簡單來說,就是在 pipe 的時候,找到語法裡的 _ ,推導出參數的位置,再把 _ 從 AST 裡拿掉。其原始碼也是個可以參考的有趣小範例。

web.ex

回到 Phoenix 裡,那些透過 use 模組名稱 ,讓這個模組變成 Controller、Router、View 的機制,就是用 macro 來完成的。而這件事在 Phoenix 中被寫在一個可以客製化的檔案裡: lib/專案名稱_web/專案名稱_web.ex

# lib/hello_phx_web/hello_phx_web.ex

defmodule HelloPhxWeb do
  @moduledoc """
    #...
  """

  def controller do
    quote do
      use Phoenix.Controller, namespace: HelloPhxWeb
      import Plug.Conn
      import HelloPhxWeb.Router.Helpers
      import HelloPhxWeb.Gettext
    end
  end

  def view do
    #...
  end

  def router do
    #...  end

  def channel do
    #...  end

  @doc """
  When used, dispatch to the appropriate controller/view/etc.
  """
  defmacro __using__(which) when is_atom(which) do
    apply(__MODULE__, which, [])
  end
end

當你在 def controller do ... endquote 區塊裡加入的任何東西,都會被嵌入每一張 controller 檔案裡。因此想要改動時要特別小心,否則發生意料之外的事,會被同事記恨的。

重點歸納

  • Elixir 的 macro 是構建整個語言的基石之一
  • 蜘蛛人法則:能力愈強,責任也愈大
  • 不要把什麼都做成 DSL
  • quote/2unquote/1defmacro/2
  • _web.ex 可以一次修改所有的 controller / router / view…

進行到這裡,Phoenix 的部份也告一個段落了。剩下的兩天將回去介紹 Elixir 比較高階的幾個功能。

Happy hacking! 明天見。


上一篇
Channel.part_2
下一篇
Elixir 中的平行運算
系列文
函數式編程: 從 Elixir & Phoenix 入門。31

1 則留言

0
taiansu
iT邦新手 5 級 ‧ 2018-01-17 03:03:00

已更新

我要留言

立即登入留言