2018/5/14

Elixir 8 Macros


Macro 可能會讓程式碼更難閱讀,如果可以使用 function,就不要用 macro。本文以 erlang/elixir 不支援的 if 語法,說明如何以 macro 實作 if 語法。


if Statement


myif «condition» do
    «evaluate if true»
else
    «evaluate if false»
end

----

myif «condition»,
    do: «evaluate if true»,
    else: «evaluate if false

可用 function 實作 if 的語法


defmodule My do
    def myif(condition, clauses) do
        do_clause = Keyword.get(clauses, :do, nil)
        else_clause = Keyword.get(clauses, :else, nil)

    case condition do
        val when val in [false, nil]
            -> else_clause
        _otherwise
            -> do_clause
        end
    end
end

$ iex my.exs
iex(1)> My.myif 1==2, do: (IO.puts "1==2"), else: (IO.puts "1 != 2")
1==2
1 != 2
:ok

但結果是不正確的,因為 elixir 同時 evaluate do: 及 else:


Macro Inject Code


defmacro 定義 macro,當傳送參數給 macro,elixir 不會直接 evaluate,而是會以 tuple 方式發送程式碼。


defmodule My do
  defmacro macro(param) do
    IO.inspect param
  end
end

defmodule Test do
  require My

  # These values represent themselves
  My.macro :atom        #=> :atom
  My.macro 1            #=> 1
  My.macro 1.0          #=> 1.0
  My.macro [1,2,3]      #=> [1,2,3]
  My.macro "binaries"   #=> "binaries"
  My.macro { 1, 2 }     #=> {1,2}
  My.macro do: 1        #=> [do: 1]

  # And these are represented by 3-element tuples,以三個元素的 tuple 表示這些 macro

  My.macro { 1,2,3,4,5 }
  # =>  {:"{}",[line: 20],[1,2,3,4,5]}

  My.macro do: ( a = 1; a+a )
  # =>  [do:
  #      {:__block__,[],
  #        [{:=,[line: 22],[{:a,[line: 22],nil},1]},
  #         {:+,[line: 22],[{:a,[line: 22],nil},{:a,[line: 22],nil}]}]}]


  My.macro do
    1+2
  else
    3+4
  end
  # =>   [do: {:+,[line: 24],[1,2]},
  #       else: {:+,[line: 26],[3,4]}]

end

$ iex dumper.exs
:atom
1
1.0
[1, 2, 3]
"binaries"
{1, 2}
[do: 1]
{:{}, [line: 29], [1, 2, 3, 4, 5]}
[do: {:__block__, [],
  [{:=, [line: 32], [{:a, [line: 32], nil}, 1]},
   {:+, [line: 32], [{:a, [line: 32], nil}, {:a, [line: 32], nil}]}]}]
[do: {:+, [line: 40], [1, 2]}, else: {:+, [line: 42], [3, 4]}]

在一個 module 定義 macro,另一個 module 使用時,必須要先用 require,這樣才能確保 macro module 在目前這個 module 前先被編譯。




quote function


quote 可讓 code 保持尚未 evaluated 的形式。quote/2 能把 Elixir 代碼轉換成底層表示形式。


iex(1)> quote do: :atom
:atom
iex(2)> quote do: 1
1
iex(3)> quote do: 1.0
1.0
iex(4)> quote do: [1,2,3]
[1, 2, 3]
iex(5)> quote do: "binaries"
"binaries"
iex(6)> quote do: {1,2}
{1, 2}
iex(7)> quote do: [do: 1]
[do: 1]

iex(8)> quote do: {1,2,3,4,5}
{:{}, [], [1, 2, 3, 4, 5]}

iex(9)> quote do: (a = 1; a + a)
{:__block__, [],
 [{:=, [], [{:a, [], Elixir}, 1]},
  {:+, [context: Elixir, import: Kernel],
   [{:a, [], Elixir}, {:a, [], Elixir}]}]}

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



eg.exs


defmodule My do
  defmacro macro(code) do
    IO.inspect code
    # 會 evaluate code
    code
  end
end
defmodule Test do
  require My
  My.macro(IO.puts("hello"))
end

eg1.exs


defmodule My do
  defmacro macro(code) do
    IO.inspect code
    # 只會 evaluate do 裡面的 code,quota 裡面的 code 會回傳給呼叫 macro 的 code,然後被 evaluate
    quote do: IO.puts "Different code"
  end
end
defmodule Test do
  require My
  My.macro(IO.puts("hello"))
end

$ elixir eg.exs
{{:., [line: 17], [{:__aliases__, [counter: 0, line: 17], [:IO]}, :puts]},
 [line: 17], ["hello"]}
hello

$ elixir eg1.exs
{{:., [line: 17], [{:__aliases__, [counter: 0, line: 17], [:IO]}, :puts]},
 [line: 17], ["hello"]}
Different code



unquote function


知道了如何獲取代碼的內部表示,那怎麼修改它呢?可利用 unquote/1 來插入新的代碼和值。當我們 unquote 一個表達式的時候,會把它運行的結果插入到 AST。


iex(1)> denominator = 2
2
iex(2)> quote do: divide(42, denominator)
{:divide, [], [42, {:denominator, [], Elixir}]}
iex(3)> quote do: divide(42, unquote(denominator))
{:divide, [], [42, 2]}

Expanding a list using unquote_splicing


# insert [3,4]
iex(4)> Code.eval_quoted(quote do: [1,2,unquote([3,4])])
{[1, 2, [3, 4]], []}

# insert 3,4 到前面的 list
iex(5)> Code.eval_quoted(quote do: [1,2,unquote_splicing([3,4])])
{[1, 2, 3, 4], []}

# '1234' 是 lists of characters
iex(6)> Code.eval_quoted(quote do: [?a, ?= ,unquote_splicing('1234')])
{'a=1234', []}


iex(7)> fragment = quote do: IO.puts("hello")
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["hello"]}
iex(8)> Code.eval_quoted fragment
hello
{:ok, []}
iex(9)> Code.eval_string("[a, a*b, c]", [a: 2, b: 3, c: 4])
{[2, 6, 4], [a: 2, b: 3, c: 4]}



myif Macro


defmodule My do
  defmacro if(condition, clauses) do
    do_clause   = Keyword.get(clauses, :do, nil)
    else_clause = Keyword.get(clauses, :else, nil)
    quote do
      case unquote(condition) do
        val when val in [false, nil] -> unquote(else_clause)
        _                            -> unquote(do_clause)
      end
    end
  end
end

defmodule Test do
  require My
  My.if 1==2 do
    IO.puts "1 == 2"
  else
    IO.puts "1 != 2"
  end
end

$ elixir myif.ex
1 != 2

References


Programming Elixir