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
沒有留言:
張貼留言