Macros
Foreword
- Macroは通常のElixirのコードよりもわかりづらい。開発者は責任を持ってクリーンなコードを書くように心がける。
- Elixirはすでに簡潔でわかりやすい仕組みを提供している。Macroを使うのは最後の手段であると理解すべきだろう。
Our first macro
- Macroは
defmacro/2
で定義される。macros.exs
を作ってみよう。
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
iex macros.exs
で作成したモジュールと使ってiexを開始して、以下のコマンドを投げてみる。
iex> require Unless
nil
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
fun_unless
は!true
の評価にもかかわらず文字列が表示されている。これはunlessが呼ばれる前にすでにIO.puts ~
が評価されているためである。もう少し詳しく見てみる。
Unless.macro_unless true, IO.puts "this should never be printed"
マクロ が呼ばれた時、macro_unless
は以下を受け取っている。
Unless.macro_unless(true, {{:., [], [{:aliases, [], [:IO]}, :puts]}, [], ["this should never be printed"]})
- そしてMacroから返される式は以下になる。
{:if, [], [
{:!, [], [true]},
{{:., [], [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
はquoted expression
を受け取り、現在の環境に合わせて拡張した結果を返す。
- このようにMacroは
quoted expression
を受け取り何かに変換して返す。実のところunless/2
, defmacro/2
, def/2
, defprotocol/2
のようなElixirのコンストラクタはすべてマクロである。すなわち、開発者の環境に応じて拡張されている。
- 我々はどのような関数やMacroもオーバーライドできる。ただし、
Kernel.SpecialForms
は例外で、拡張はできない。
Macros hygiene
- ElixirのMacroは遅延評価(late resolutionはこの訳でいいのか。。。)を持っている。これは
quoted expression
の中の変数とMacroが拡張されるときの変数がコンフリクトしないことを保証している。以下の例を見てみる。
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
no_interference/0
の中でa
に値を束縛しているが、require
しているTestモジュールのa
には影響を与えていない。
- 次のようにするとどうだろうか。
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
- Macro内の
a
をvar!
で囲むとgo/0
内のaに値を再束縛している。
The environment
Macro.expand_once/2
を使った時に引数に __ENV__
を指定したが、これはなんだろうか。
__ENV__
は、現在のモジュールに関する様々な情報が格納された Macro.Env
構造体のインスタンスを返す。
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]
Private macros
- Elixirは
Private Macro
もサポートしている。モジュール内がスコープになるMacroだが、注意しなければならないことがある。
- Macroは使う前に定義されていなければならない。例えば以下のような場合は、Macroを定義する前に使っているのでエラーになる。
iex> defmodule Sample do
...> def four, do: two + two
...> defmacrop two, do: 2
...> end
** (CompileError) iex:2: function two/0 undefined
Write macros responsibly
- Macroは強力で、ElixirはMacroを責任を持って使うための仕組みを提供している。
- Macros are hygienic
- Macroの中で定義された変数はユーザーコードに影響されない。
- さらにMacroの中での関数呼び出しや、aliasはユーザーのコンテキストを汚染しない
- Macros are lexical
- Macroはグローバル空間に注入することはできない。Macroはモジュールの中で
require
あるいはimport
しないと使えない。
- Macros are explicit
- Macroは明示的に実装されていなければ動かすことができない。
- Macroはコンパイルの間に明示的にもたらされなければならない。
- Macros’ language is clear
- 多くの言語は
quote
やunquote
のためにシンタックスショートカットを提供している。ElixirはMacroの定義やquoted expression
との境界を明確にするために、別に明示的に書くようにしている。
- 上記の仕組みを持ったとしても、開発者は大きな責任を負う。もしMacroを使う場合は、MacroはあなたのAPIではないということを覚えておこう。Macroの定義は短く保つべきである。次の例を見てみよう。
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
- Macro内のコードが明確でテストしやすくなった。テストを書く場合は
do_this_that_and_that/3
を直接呼べばよい。