Help us understand the problem. What is going on with this article?

Elixirの"var!"マクロによるnilコンテキスト引数の考察

More than 3 years have passed since last update.

ElixirにはMacro hygieneという仕組みがあり、マクロが展開された箇所に同名の変数があってもその変数を「汚さない」ようにコンパイルしてくれます。

Macros 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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away