スポンサーリンク

2015年8月27日

[Elixir]__using__について分かったこと

とある錬金術師の万能薬(Elixir)

Goal

マクロで使われている、__using__について分かったことをまとめる。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4

Wait a minute

Elixirのマクロが分からない・・・orz
メタプログラミングが分からない・・・orz
正直、elixir-langのGetting Startにあるメタプログラミングが終わった後、
どうやって手を付けていけばいいのかさっぱり分からない。
一応、最低限(?)の使う機能については知っているが、その程度・・・
でも、その後どうやって学んでいけばいいか分からない。
以前、紹介した本?初心者いきなり読めるのかね・・・
まだ友人も手に入ってないみたいだから、しばらく先だしな~
仕方ないから一番簡単で理解できそうな部分からやる。
そこから色々と広げていけるか分からないが、ある部分から始める。

Index

|> About __using__
|> About __MODULE__
|> Apply a little bit
|> @before_compile & __before_compile__
|> Extra

About __using__

よくマクロを使ったソースコードで以下のような記述を見かけないだろうか?

Example:

defmodule UsingModule do
  defmacro __using__(_opts) do
    quote do
      ...
    end
  end
end
私が理解できる部分はここが(今の)限界らしい(笑わば笑え!)
とりあえず、この部分の理解をすることが今回やることです。
これ何やってんの?って話だが・・・
このモジュールをuseした時に展開される部分とのこと。
以下のようなuseした時。

Example:

defmodule UseModule do
  use ModuleSample
end
例えば、importやaliasなんかをここでやっていたら?

Example:

defmodule ImportModule do
  def import_module do
    IO.puts "ImportModule.import_module/0"
  end
end

defmodule UsingModule do
  defmacro __using__(_opts) do
    quote do
      import ImportModule
    end
  end
end

defmodule UseModule do
  use UsingModule

  def test do
    import_module
  end
end

Result:

iex> UseModule.test
ImportModule.import_module/0
:ok
インポートされているので、ImportModuleの関数を使えていますね。
もう少し、この部分の展開を見てみる。
defmodule UseModule do
  use UsingModule

  def test do
    import_module
  end
end
つまり、useの部分を展開するとこうなっているわけですね。
defmodule UseModule do
  import ImportModule

  def test do
    import_module
  end
end
これを少し応用して使ってみる前に・・・一つ別のことをやる。

About __MODULE__

これも割とよく見かける記述ですね。
これの値がどうなっているの確認してみよう。

Example:

defmodule ModuleSample do
  def get_module, do: __MODULE__end

Result:

iex> ModuleSample.get_module
ModuleSample
モジュールそのもの(?)が値として入っているようですね。

Description:

そのままiex上で入力してみると・・・値はnilのようです。
iex> __MODULE__
nil

Apply a little bit

useの時にオプション引数を指定する。
また、オプションの内容を表示してみる。

Example:

defmodule ImportModule do
  def import_module do
    IO.puts "ImportModule.import_module/0"
  end
end

defmodule UsingModule do
  defmacro __using__(opts) do
    quote do
      import ImportModule

      @default_value unquote(opts)

      def get_default_value do
        @default_value
      end
    end
  end
end

defmodule UseModule do
  use UsingModule, a: 1, b: 2, c: 3

  def test do
    import_module
    get_default_value
  end
end

Description:

get_default_value/0の関数だが、
マクロが展開された時に定義される。
そのため、展開先のUseModuleの関数となる。
UsingModuleで定義しているが、UsingModuleの関数でないのは注意。

Result:

iex> UseModule.test
ImportModule.sample/0
[a: 1, b: 2, c: 3]
先ほどの、__MODULE__を使って自分自身をimportしてみる。

Example:

defmodule UsingModule do
  defmacro __using__(opts) do
    quote do
      import ImportModule
      import unquote(__MODULE__)

      @default_value unquote(opts)

      def get_default_value do
        IO.inspect @default_value
      end
    end
  end

  def using_module do
    IO.puts "UsingModule.using_module/0"
  end
end

defmodule UseModule do
  use UsingModule, a: 1, b: 2, c: 3

  def test do
    import_module
    get_default_value
    using_module
  end
end

Result:

iex> UseModule.test
ImportModule.import_module/0
[a: 1, b: 2, c: 3]
UsingModule.using_module/0
:ok

@before_compile & __before_compile__

あまり理解できてないので、とりあえず使い方だけ。

Example:

defmodule UsingModule do
  defmacro __using__(opts) do
    quote do
      Module.register_attribute(__MODULE__, :default_value, accumulate: :false)

      import ImportModule
      import unquote(__MODULE__)
      @before_compile unquote(__MODULE__)
      @default_value unquote(opts)
    end
  end

  defmacro __before_compile__(env) do
    default_value = Module.get_attribute(env.module, :default_value)
    quote do
      def get_default_value do
        IO.inspect unquote(default_value)
      end
    end
  end

  def using_module do
    IO.puts "UsingModule.using_module/0"
  end
end

Result:

iex> UseModule.test
ImportModule.import_module/0
[a: 1, b: 2, c: 3]
UsingModule.using_module/0
:ok

Description:

表示結果は変わっていないが、__using__で関数の定義をしていない。
__before_compile__でget_default_value/0を定義している。
そのため展開先が、UsingModuleに変わっている。
使った機能について・・・
  • register_attribute/3
    属性を登録できる関数。
    登録した属性: @default_value
  • get_attribute/2
    モジュールから指定した属性を取得できる関数。
  • @before_compile
    モジュールがコンパイルされる前に呼び出されるフック。
    本体(?)は、__before_compile__/1の部分。
注意点として、@after_compileとは異なり、
コールバック関数 / マクロは別のモジュールへ配置する必要がある。
(コールバックが呼び出される時には、まだモジュールが存在していないため)
別にだから何?って内容で、
メタプロと言っていいかさえ怪しいプログラムだが。
使い方だけ把握したけど、使い道がいまいち分からんな・・・

Extra

__using__を展開させてみた。

Example:

iex> require UsingModule
nil
iex> expr = quote do: UsingModule.__using__([])
{{:., [], [{:__aliases__, [alias: false], [:UsingModule]}, :__using__]}, [],
 [[]]}
iex> Macro.expand_once(expr, __ENV__)
{:__block__, [],
 [{{:., [],
    [{:__aliases__, [alias: false, counter: 16], [:Module]},
     :register_attribute]}, [],
   [{:__MODULE__, [counter: 16], UsingModule}, :default_value,
    [accumulate: false]]},
  {:import, [context: UsingModule, counter: 16],
   [{:__aliases__, [alias: false, counter: 16], [:ImportModule]}]},
  {:import, [context: UsingModule, counter: 16], [UsingModule]},
  {:@, [context: UsingModule, import: Kernel],
   [{:before_compile, [], [UsingModule]}]},
  {:@, [context: UsingModule, import: Kernel], [{:default_value, [], [[]]}]}]}
iex> Macro.to_string(Macro.expand_once(expr, __ENV__))
"(\n  Module.register_attribute(__MODULE__, :default_value, accumulate: false)\n  import(ImportModule)\n  import(UsingModule)\n  @before_compile(UsingModule)\n  @default_value([])\
n)"

Speaking to oneself

私のメタプログラミングの知識は、C++のテンプレートで止まっています・・・
(当時もバリバリ使ってたわけでも使えていたわけでもない)
Qiitaでマクロの展開手順などを記事にされている方々のように、
展開して追っていくことができるなら、少しは捗る気がします。
でも、展開をさせていく方法がいまいち分からない。
分かったてはいたが、メタプログラミングの頭もできてなければ、知識も足りない。
道のりは遠い・・・

Bibliography

人気の投稿