loading...
gumi TECH Blog

Elixirメタプログラミング 02: マクロ

gumitech profile image gumi TECH ・4 min read

本稿はElixir公式サイトの許諾を得て「Macros」の解説にもとづき、加筆補正を加えて、Elixirにおけるマクロの定義方法についてご説明します。

はじめに

Elixirには、マクロをできるだけ安全に使える環境が整えられています。とはいえ、マクロでクリーンなコードを書くことは開発者の責任です。マクロをつくるのは、通常のElixirの関数を使うより難しいといえます。むやみにマクロを用いるのは、避けた方がよいでしょう。

Elixirには、データ構造や関数によりわかりやすく読みやすいコードを書ける仕組みがすでに備わっています。コードは黙示的より明示的に、短くよりわかりやすくすべきです。マクロはどうしても必要な場合にお使いください。

はじめてのマクロ

Elixirのマクロはdefmacro/2により定めます。本稿では、コードを基本的に.exsファイルに書いて、elixir ファイル名またはiex ファイル名のコマンドで実行しましょう。

簡単なマクロを書いて、動きを確かめてみましょう。モジュールはmacros.exsに定め、マクロと関数を加えます。コードの中身はどちらも同じです。反転した条件に応じて、引数の式を実行します。

defmodule Unless do
  def fun_unless(clause, do: expression) do
    if(!clause, do: expression)
  end

  defmacro macro_unless(clause, do: expression) do
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end
end

関数は、受け取った引数をif/2に渡します。けれど、マクロが受け取るのは内部表現です(「Elixirメタプログラミング 01: 内部表現 ー quote/2とunquote/1」参照)。そして、それを差し込んだ別の内部表現を返します。

前述で定義したマクロを試すために、このモジュールでiexを開きましょう。

$ iex macros.exs

マクロを使うには、その前にrequire/2でモジュールを要求しなければなりません。そのあと、関数と同じように呼び出せます。

iex> require Unless
Unless
iex> Unless.macro_unless(true, do: IO.puts "this should never be printed")
nil
iex> Unless.fun_unless(true, do: IO.puts "this should never be printed")
this should never be printed
nil

マクロも関数も戻り値(nil)は同じでした。けれど、マクロはIO.puts/2に渡した文字列が出力されません。文字列が関数で出力されたのは、値を返す前に引数が評価されるからです。これに対して、マクロは渡された引数を評価しません。引数は内部表現として受け取られ、別の内部表現にされるのです。今回、定義したmacro_unlessマクロは、ifの内部表現になります。

前掲macro_unlessの呼び出しは、引数につぎのような内部表現を用いたのと同じです。

iex> Unless.macro_unless(
...>    true,
...>    [
...>      do: {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [],
...>       ["this should never be printed"]}
...>    ]
...> )

さらに、マクロの定義も内部表現に展開すると、つぎのようになります。

{:if, [context: Unless, import: Kernel],
 [
   {:!, [context: Unless, import: Kernel], [true]},
   [
     do: {{:., [],
       [
         {:__aliases__, [alias: false, counter: -576460752303422719], [:IO]},
         :puts
       ]}, [], ["this should never be printed"]}
   ]
 ]}

引数の内部表現は、quote/2で確かめられるでしょう。さらに、その内部表現を展開するのがMacro.expand_once/2です。

iex> expr = quote do: Unless.macro_unless(true, do: IO.puts "this should never be printed")
{{:., [], [{:__aliases__, [alias: false], [:Unless]}, :macro_unless]}, [],
 [
   true,
   [
     do: {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [],
      ["this should never be printed"]}
   ]
 ]}
iex> res  = Macro.expand_once(expr, __ENV__)
{:if, [context: Unless, import: Kernel],
 [
   {:!, [context: Unless, import: Kernel], [true]},
   [
     do: {{:., [],
       [
         {:__aliases__, [alias: false, counter: -576460752303422719], [:IO]},
         :puts
       ]}, [], ["this should never be printed"]}
   ]
 ]}
iex> IO.puts Macro.to_string(res)
if(!true) do
  IO.puts("this should never be printed")
end
:ok
iex> IO.puts Macro.to_string(expr)
Unless.macro_unless(true) do
  IO.puts("this should never be printed")
end
:ok

Macro.expand_once/2は内部表現を受け取って、現在の環境に応じて展開します。前述の例では、マクロUnless.macro_unless/2が展開されて呼び出され、結果が返りました(__ENV__については後述します)。さらに戻り値の内部表現をIO.puts/2で文字列に出力して確かめたということです。

なお、内部表現をコードの文字列表現として確かめたいときは、つぎのように書くと簡単です。

iex> expr |> Macro.expand_once(__ENV__) |> Macro.to_string |> IO.puts
if(!true) do
  IO.puts("this should never be printed")
end
:ok

以上が、マクロの基本的な働きです。内部表現を受け取って、別のものに変換するという役割を果たします。実際、Elixirのunless/2の実装はつぎのようなものです。

defmacro unless(clause, do: expression) do
  quote do
    if(!unquote(clause), do: unquote(expression))
  end
end

unless/2defmacro/2def/2defprotocol/2などの構文、その他公式サイトのガイドに掲げられているコードは、純粋なElixirに加え、マクロで実装されているものも少なくありません。言語を構築している構文は、開発者がそれぞれ開発しているドメインに言語を拡張するために用いることもできます。

関数やマクロを用途に応じて定め、さらにElixirに組み込み済みの定義を上書きすることもできます。ただし、Elixirの特殊フォームだけは例外です。Elixirで実装されていないため、上書きができません。特殊フォームに何があるか、詳しくは「Kernel.SpecialForms」をご参照ください。

マクロの健全さ

Elixirのマクロは、あとで解決されます。つまり、マクロで定められた変数は、マクロが展開されるコンテキストに定義された変数と競合することはないということです。

たとえば、つぎのようにマクロと関数を、それぞれ別のモジュールに定義したとします。

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

関数が変数に値を定義したあと、呼び出したマクロが同名の変数に異なる値を与えても、関数の変数値は変わりません。

iex> HygieneTest.go
13

あえて、マクロが呼び出されたコンテキストに影響を与えたいときには、var!/2を使ってください。

defmodule Hygiene do
  # defmacro no_interference do
  defmacro interference do
    # quote do: a = 1
    quote do: var!(a) = 1
  end
end

defmodule HygieneTest do
  def go do
    require Hygiene
    a = 13
    # Hygiene.no_interference
    Hygiene.interference
    a
  end
end

マクロがvar/2に与えた変数は、呼び出されたコンテキストに上書きして定義されます。

iex> HygieneTest.go
1

上書きされたもとの変数値は使われません。そのため、コンパイル時に、それを告げる警告が示されます。

warning: variable "a" is unused

変数のコンテキストは、内部表現の第3要素のアトムで示されます。そして、モジュールからquote/2で引用された変数は、そのモジュールをコンテキストにもつのです。そのため、他のコンテキストを汚すことなく健全さが保たれます(「健全なマクロ」参照)。

defmodule Sample do
  def quoted do
    quote do: x
  end
end
iex> quote do: x
{:x, [], Elixir}
iex> Sample.quoted
{:x, [], Sample}

Elixirはインポートとエイリアスにも、同じ仕組みを与えます。マクロはもとのモジュールのもとで動作し、展開された先と競合することはありません。あえて、影響を及したいときに用いるのが、var!/2alias!/1です。ただし、健全さが失われ、使われる環境を直接変えることになりますので、ご注意ください。

Macro.var/2を使うと、動的に変数をつくることができます。第1引数が変数名で、第2引数はコンテキストです。

defmodule Sample do
  defmacro initialize_to_char_count(variables) do
    Enum.map variables, fn(name) ->
      var = Macro.var(name, nil)
      length = name |> Atom.to_string |> String.length
      quote do
        unquote(var) = unquote(length)
      end
    end
  end

  def run do
    initialize_to_char_count [:red, :green, :yellow]
    [red, green, yellow]
  end
end
iex> Sample.run
[3, 5, 6]

環境

前述「はじめてのマクロ」の項でMacro.expand_once/2の第2引数に__ENV__を渡しました。戻り値はMacro.Env構造体のインスタンスです。構造体にはコンパイル環境の有用な情報が納められています。たとえば、現在のモジュールやファイル、行番号、現在のスコープのすべての変数などです。import/2require/2で加わったものも含まれます。

iex> __ENV__.module
nil
iex> __ENV__.file  
"iex"
iex> __ENV__.requires
[IEx.Helpers, Kernel, Kernel.Typespec]
iex> require Integer
Integer
iex> __ENV__.requires
[IEx.Helpers, Integer, Kernel, Kernel.Typespec]

Macroモジュールの多くの関数は環境を与えて呼び出します。詳しくは、「Macro」をご参照ください。また、コンパイル環境については「Macro.Env」で解説されています。

プライベートマクロ

Elixirはdefmacrop/2で、プライベートマクロが定義されます。プライベートな関数になりますので、マクロを定義したモジュールの中で、コンパイル時にしか使えません。

defmodule Sample do
  defmacrop two, do: 2
  def four, do: two + two
end
iex> Sample.four
4

そして、プライベートマクロは、使う前に定義されていることが必要です。マクロは展開されてから、関数として呼び出せます。そのため、定義の前に呼び出すと、エラーが生じるのです。

defmodule Sample do
  def four, do: two + two  # ** (CompileError) macros.exs: undefined function two/0
  defmacrop two, do: 2
end

責任のあるマクロを書く

マクロはできることが豊富な構文です。Elixirはさまざまな仕組みで、責任のあるマクロが書けるようにしています。

  • 健全: デフォルトでは、マクロ内で定義された変数は、使う側のコードに影響を与えません。さらに、マクロのコンテキストにおける関数呼び出しやエイリアスも、ユーザーコンテキストからは切り離されます。
  • レキシカル: コードやマクロをグローバルに差し込むことはできません。マクロが定められたモジュールを、明示的にrequire/2またはimport/2で使う必要があります。
  • 明示: マクロは明示的に呼び出さなければ実行できません。言語によってはパースやリフレクションなどといった仕組みも用いて、開発者が外からわからないように関数をすっかり書き替えられたりします。Elixirのマクロは、呼び出す側がコンパイルのとき明示的に実行しなければならないのです。
  • 明確: 多くの言語にはquoteunquoteに省略記法が備えられています。Elixirではフルに入力することにしました。マクロ定義と内部表現をはっきりと識別できるようにするためです。

このような仕組みはあるものの、マクロを書く責任の多くは開発者が担います。マクロの助けがいると判断した場合、マクロがAPIではないことは頭においてください。

マクロの定義は、内部表現も含めて短くしましょう。つぎのように書くのは、よくない例です。

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

つぎのように書けば、コードは明確になり、テストや管理もしやすくなります。関数do_this_that_and_that/3は直接呼び出してテストできるからです。また、マクロに依存したくない開発者向けのAPIを設計するのにも役立つでしょう。

defmodule MyModule do
  defmacro my_macro(a, b, c) do
    quote do
      # マクロに書くのは最小限に
      # その他の処理はすべて関数に
      MyModule.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

Posted on by:

gumitech profile

gumi TECH

@gumitech

gumi TECH は、株式会社gumiのエンジニアによる技術記事公開やDrinkupイベントなどの技術者交流を行うアカウントです。 gumi TECH Blog: http://dev.to/gumi / gumi TECH Drinkup: http://gumitech.connpass.com

gumi TECH Blog

株式会社gumiのエンジニアによる技術記事を公開しています。

Discussion

markdown guide