11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

gumi Inc.Advent Calendar 2018

Day 25

Elixir のマクロを読もう2

Last updated at Posted at 2018-12-23

Merry Christmas!

gumi Inc. 2018 Advent Calendar の栄えある最終日を担当いたします幾田です. よろしくお願いします. 初日は Elixir で幕開けさせていただきましたが, 最終日も Elixir で幕引きさせていただきます.

この記事について

この記事は, Elixir のマクロを読もう1 から始まった 1 Elixir のマクロを読もうシリーズの二回目となります.

前回の記事 では, マクロ定義の基礎的な概念 (AST) や道具 (quote/2, unquote/1, defmacro/2) について解説したため, この記事では, もう少し踏み込んだ解説を行っていきます.

この記事を読み終えると, gen_server.ex に定義されている GenServer モジュールのマクロに関連するコードが読めるようになります.

Elixir マクロ初心者の方は, この記事から読み始めることはできません. かならず, 前回の記事 から順番に読み進めてください.

require/2

マクロが定義される目的として, 最多のものは「def 定義をモジュールに取り込む」ことである.

単に def 定義を取り込むだけであれば, require/2 や import/2 で def 定義を差し込むマクロを使用可能にし, そのマクロを使用するとよい.

require/2, import/2 の詳細ついては, こちら を参照すること.

次のファイルを保存し, iex 上で動作確認してほしい.

exsample01.ex
defmodule ExtendChristmasDef do
  defmacro extend_christmas_def do
    quote do
      def say_greeting_for_christmas do
        IO.puts "Merry Christmas!"
      end
    end
  end
end

defmodule Greeting do
  require ExtendChristmasDef # 定義済のマクロを使用可能にする
  ExtendChristmasDef.extend_christmas_def # マクロで say_greeting/0 を取り込む
end
iex> c "exsample01.ex"
[Greeting, ExtendChristmasDef]

iex> Greeting.say_greeting_for_christmas
Merry Christmas!
:ok

use/2

筆者は, require/2 を使用する方が明示的で好ましいと考えるが, use/2 を用いることで前述を少しだけ短縮できる.

次のファイルを保存し, iex 上で動作確認してほしい.

exsample02.ex
defmodule ExtendChristmasDef do
  defmacro __using__(_options) do
    quote do
      def say_greeting_for_christmas do
        IO.puts "Merry Christmas!"
      end
    end
  end
end

defmodule Greeting do
  use ExtendChristmasDef # なんと一行に!
end
iex> c "exsample02.ex"
[Greeting, ExtendChristmasDef]

iex> Greeting.say_greeting_for_christmas
Merry Christmas!
:ok

use/2 とは何だろうか? use/2 の正体を探るため, iex を起動し, 次のコードを実行して結果を確認してほしい.

iex> code_expression = quote do: use ExtendChristmasDef

iex> code_expression |> Macro.expand_once(__ENV__) |> Macro.to_string
(
  require(ExtendChristmasDef)
  ExtendChristmasDef.__using__([])
)

iex> code_expression = quote do: use ExtendChristmasDef, foo: :bar, baz: :quu

iex> code_expression |> Macro.expand_once(__ENV__) |> Macro.to_string
(
  require(ExtendChristmasDef)
  ExtendChristmasDef.__using__(foo: :bar, baz: :quu)
)

見てのとおり use/2 は, require/2 と __using__/1 に展開される.

use/2 は一般的によく使用されるのだが, Ecto や Plug などでは, __using__/1 内で use/2, import/2, alias/2 などが多用されており, 正確に挙動を把握するには, かなり広範囲に渡りコードを読む必要がある. マクロの辛さは, use/2 に凝縮されている.

なお, Macro.expand_once/2 は, コード表現に一回だけマクロを適用して展開するため, このようにマクロの動作確認などに使用できる. 最後まで展開する Macro.expand/2 も存在する.

Macro.to_string/1 は, 前回の記事 を参照すること.

@before_compile

__using__/1 内でよく行われることに, モジュールアトリビュートの操作がある.

次のファイルを保存し, iex 上で動作確認してほしい.

exsample03.ex
defmodule ExtendGreetDef do
  defmacro __using__(_options) do
    quote do
      # greet/2 マクロを簡単に呼べるよう import/2 を使用
      import unquote(__MODULE__)

      # モジュールのアトリビュートを登録
      # リストとして定義
      # greet/2 マクロで定義された def を保持する
      Module.register_attribute __MODULE__, :greet_defs, accumulate: true

      # greet/2 マクロで定義された def を全て実行する
      def say do
        Enum.each @greet_defs, fn greet_def ->
          apply(__MODULE__, greet_def, [])
        end
      end
    end
  end

  defmacro greet(name, message) do
    quote do
      @greet_defs unquote(name) # アトリビュートに def の名前を追加
      def unquote(name)() do
        IO.puts unquote(message)
      end
    end
  end
end

defmodule Greeting do
  use ExtendGreetDef

  greet :for_christmas, "Merry Christmas!"
  greet :for_new_year, "Happy New Year!"
end
iex> c "exsample03.ex"
[Greeting, ExtendGreetDef]

iex> Greeting.for_christmas
Merry Christmas!
:ok

iex> Greeting.for_new_year
Happy New Year!
:ok

iex> Greeting.say
:ok

greet/2 による def 定義までは意図した通りに動作しているが, 最後に @greet_defs に登録されている def を実行するところが意図した通りに動作していない. これは, say/0 の定義時に @greet_defs が空リストであることが原因である.

解決策として, greet/2 による @greet_defs へのリスト追加を全て終えてから say/0 を定義できるとよい. これを実現するため, 全てのマクロ展開が終わった後, かつ, コンパイルの寸前に呼ばれるフック @before_compile を使用する.

次のファイルを保存し, iex 上で動作確認してほしい.

exsample04.ex
defmodule ExtendGreetDef do
  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)

      Module.register_attribute __MODULE__, :greet_defs, accumulate: true

      # このアトリビュートにモジュール名を指定すると,
      # コンパイル前に, __before_compile__/1 が実行される.
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(_env) do
    # __before_compile__ 返されたコード表現は,
    # 呼び出したモジュールの末尾に追加される.
    quote do
      def say do
        Enum.each @greet_defs, fn greet_def ->
          apply(__MODULE__, greet_def, [])
        end
      end
    end
  end

  defmacro greet(name, message) do
    quote do
      @greet_defs unquote(name)
      def unquote(name)() do
        IO.puts unquote(message)
      end
    end
  end
end

defmodule Greeting do
  use ExtendGreetDef

  greet :for_christmas, "Merry Christmas!"
  greet :for_new_year, "Happy New Year!"
end
iex> c "exsample04.ex"
[Greeting, ExtendGreetDef]

iex> Greeting.say
Happy New Year!
Merry Christmas!
:ok

正確ではないが, マクロが展開されていく大凡のイメージを次に示す.

# マクロ展開前

defmodule Greeting do
  use ExtendGreetDef

  greet :for_christmas, "Merry Christmas!"
  greet :for_new_year, "Happy New Year!"
end

# use が展開される

defmodule Greeting do
  require(ExtendGreetDef)
  ExtendGreetDef.__using__([])

  greet :for_christmas, "Merry Christmas!"
  greet :for_new_year, "Happy New Year!"
end

# __using__/1 が展開される
# require/2 は特殊であるため割愛

defmodule Greeting do
  import ExtendGreetDef

  Module.register_attribute ExtendGreetDef, :greet_defs, accumulate: true

  @before_compile ExtendGreetDef

  greet :for_christmas, "Merry Christmas!"
  greet :for_new_year, "Happy New Year!"
end

# greet が展開される
# import/2 は特殊であるため割愛

defmodule Greeting do
  Module.register_attribute ExtendGreetDef, :greet_defs, accumulate: true

  @before_compile ExtendGreetDef

  @greet_defs :for_christmas
  def for_christmas() do
    IO.puts "Merry Christmas!"
  end

  @greet_defs :for_new_year
  def for_new_year() do
    IO.puts "Happy New Year!"
  end
end

# @before_compile が処理される

defmodule Greeting do
  Module.register_attribute ExtendGreetDef, :greet_defs, accumulate: true

  @greet_defs :for_christmas
  def for_christmas() do
    IO.puts "Merry Christmas!"
  end

  @greet_defs :for_new_year
  def for_new_year() do
    IO.puts "Happy New Year!"
  end

  def say do
    Enum.each @greet_defs, fn greet_def ->
      apply(__MODULE__, greet_def, [])
    end
  end
end

# @greet_defs が展開される

defmodule Greeting do
  def for_christmas() do
    IO.puts "Merry Christmas!"
  end

  def for_new_year() do
    IO.puts "Happy New Year!"
  end

  def say do
    Enum.each [:for_new_year, :for_christmas], fn greet_def ->
      apply(__MODULE__, greet_def, [])
    end
  end
end

おわりに

ここまでで, GenServer モジュールのマクロに関連するコードが読めるようになるため, 今回のマクロ解説は終わりとします.

Advent Calendar は終わってしまいますが, Elixir のマクロを読もうシリーズは続けていく予定です. もう少々お付き合いいただければ幸いです. 次回は, GenServer モジュールの解説か, defmacro/2 を使わないマクロ定義の解説を行う予定です.

追記

今回も記事を書いている最中に, 基礎情報の範囲外として除外した内容を追記します.

use/2 のコードを読む

Macro.expand/2 で展開結果だけを眺めるのではなく, コードを読んで動作を把握しておくとよい. use/2 は, kernel.ex で定義されている.

なお, 次のコードは, この記事を執筆時点(2018年12月23日)の Elixir master ブランチのものである.

kernel.ex
defmacro use(module, opts \\ []) do
  calls =
    Enum.map(expand_aliases(module, __CALLER__), fn
      expanded when is_atom(expanded) ->
        quote do
          require unquote(expanded)
          unquote(expanded).__using__(unquote(opts))
        end

      _otherwise ->
        raise ArgumentError,
              "invalid arguments for use, " <>
                "expected a compile time atom or alias, got: #{Macro.to_string(module)}"
    end)

  quote(do: (unquote_splicing(calls)))
end

defp expand_aliases({{:., _, [base, :{}]}, _, refs}, env) do
  base = Macro.expand(base, env)

  Enum.map(refs, fn
    {:__aliases__, _, ref} ->
      Module.concat([base | ref])

    ref when is_atom(ref) ->
      Module.concat(base, ref)

    other ->
      other
  end)
end

defp expand_aliases(module, env) do
  [Macro.expand(module, env)]
end

expand_aliases/2 で, コード表現で渡ってくるモジュール名をアトムに戻している. ここで注目するべきはリスト処理となっていること.

iex を起動し, 次のコードを実行して結果を確認してほしい.

iex> code_expression = quote do: A.B.C
{:__aliases__, [alias: false], [:A, :B, :C]}

iex> Macro.expand(code_expression, __ENV__)
A.B.C

A.B.C のような一般的なモジュール名であれば, Macro.expand/2 でアトムに戻る.

では, なぜリスト処理なのか? これは, 「{{:., _, [base, :{}]}, _, refs}」というコード表現のパース処理にヒントがある.

iex を起動し, 次のコードを実行して結果を確認してほしい.

iex> quote do: A.{B, C}
{{:., [], [{:__aliases__, [alias: false], [:A]}, :{}]}, [],
 [{:__aliases__, [alias: false], [:B]}, {:__aliases__, [alias: false], [:C]}]}

:{} がコード表現に入るパターンは, タプルであるため, タプルを利用した複数モジュールを一括処理する構文のためだと勘が働く.2

とはいえ, commit を読めば一目瞭然ではある.

結果, expand_aliases/2 に A.{B, C} を渡すと [A.B, A.C] というリストが戻り, Enum.map/2 から require/2 と __using__/1 するコード表現をリストで受け取ることになる.

最後に, unquote_splicing/1 でコード表現のリストを, コード表現に戻す.

# calls の値イメージ (実際には, コード表現が入っている)

[(
  require(A.B)
  A.B.__using__([])
), (
  require(A.C)
  A.C.__using__([])
)]


# unquote_splicing/1 を行った後の値イメージ

(
  (
    require(A.B)
    A.B.__using__([])
  )
  (
    require(A.C)
    A.C.__using__([])
  )
)
  1. 続くと良いな. 続くようにがんばります.

  2. 勘が働かない? blame して commit を読みましょう.

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?