ElixirにはMacro hygieneという仕組みがあり、マクロが展開された箇所に同名の変数があってもその変数を「汚さない」ようにコンパイルしてくれます。
そのままのコピペですが、要するに下記のような動作になるということです。
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
上記ページの説明、およびこちらのスレッドのJose Valimさんの解説を読む限り、上記コードのHygieneTestモジュール内で定義された変数と、Hygieneモジュール内で定義された変数は別々のコンテキストになるため、これによって別々の変数として扱えるという仕組みになっているようです。
ですが、var!
というnil contextマクロを使用すると、quote内の変数のコンテキストをnilに設定することが可能です。この場合下記のような動作になります。
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
quote内の変数のコンテキストがnilに設定されると、その変数には「展開されたモジュールのコンテキスト」が割り当てられるようです。この場合、同じ変数名の変数は「同一の変数である」と解釈され、上記のような結果になるようです。
この後ちょっとややこしいサンプルが出てきますが、原則はとにかく
quote内でvar!を使用すると、その変数のコンテキストはnilになる
nilコンテキストの変数には、展開された側のモジュールのコンテキストが割り当てられる
です。
nilコンテキスト引数の例1
Elixirのドキュメントでは説明されていませんが、var!はquote内で定義される関数の引数に指定することが可能です。つまり関数の引数として「nilコンテキストの引数」を指定することが出来ます。
defmodule Mod1 do
defmacro macro1 do
quote location: :keep do
def func1(var!(p)) do # <= nilコンテキストの引数を宣言
Mod1.macro2
end
end
end
defmacro macro2 do
quote location: :keep do
IO.inspect var!(p)
end
end
end
defmodule Mod1test do
require Mod1
Mod1.macro1
def test do
func1("a")
end
end
Mod1test.test
# => "a"
上記はex_adminで多用されている方法ですが、これが何を意図した処理かというと、
macro1内のfunc1関数呼び出し時に宣言&束縛されたnilコンテキスト変数pを、macro1から展開される他のマクロでも使用できるようにしている
ということのようです。(おそらくDSLを作成する際にこの手法が必要だったのだと思われます)
def func1(var!(p)) do
の箇所がかなり分かりにくいと思いますが、要するにdef func1(p) do
の引数pにnilコンテキストを指定しているということです。
var!
が指定されない場合、引数pのコンテキストはMod1になりますが、def func1(var!(p)) do
だと、引数pのコンテキストはマクロが展開される側のモジュール、つまりMod1testになるわけです。
IO.inspect var!(p)
も同様です。var!によってnilコンテキストが指定されているので展開されるとMod1testのコンテキストで動作し、またpを定義しているfunc1から展開されていますのでpにアクセスすることが出来る、というわけです。
nilコンテキスト引数の例2
defmodule Mod2 do
defmacro macro3(do: block) do
quote location: :keep do
def func3(var!(p)) do
unquote(block)
end
end
end
end
defmodule Mod2test do
import Mod2
macro3 do
IO.inspect p
end
end
Mytest.func3("a")
# => "a"
上記も一見ややこしいですが、
quote内でvar!を使用すると、その変数のコンテキストはnilになる
nilコンテキストの変数には、展開された側のモジュールのコンテキストが割り当てられる
という原則に従って読めば理解出来ると思います。
IO.inspect p
の箇所では、この時点ではまだ存在していない変数pを参照していますが、macro3 do
の部分は、展開されると
defmodule Mod2test do
def func3(var!(p)) do
IO.inspect p
end
end
上記のようになる(あくまでもイメージですが)ので、func3の引数pが参照出来るようになり、コンパイルが通るということになります。
最後に
Elixirの色々なパッケージのコードを解読するにはマクロの知識が必須ですが、その中でもvar!
はかなりの関門になると思いますので、こちらの情報が何かの一助になれば幸いです。
ちなみに上記はあくまで「考察」に過ぎませんのでその点ご了承頂けますと幸いです。誤り等ありましたらご指摘頂けますと大変有り難いですm(__)m