Elixirメタプログラミング 02: マクロ
Tags: elixir, webdev, tutorial, programming
Published: 2019-12-20 08:53:00

---
title: Elixirメタプログラミング 02: マクロ
published: true
description: Elixir公式サイトの許諾を得て「Macros」の解説にもとづき、加筆補正を加えて、Elixirにおけるマクロの定め方と使い方についてご説明します。
tags: #elixir, #webdev, #tutorial, #programming
---

本稿はElixir公式サイトの許諾を得て「[Macros](https://elixir-lang.org/getting-started/meta/macros.html)」の解説にもとづき、加筆補正を加えて、Elixirにおけるマクロの定義方法についてご説明します。

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

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


# はじめてのマクロ
Elixirのマクロは[`defmacro/2`](https://hexdocs.pm/elixir/Kernel.html#defmacro/2)により定めます。本稿では、コードを基本的に`.exs`ファイルに書いて、`elixir ファイル名`または`iex ファイル名`のコマンドで実行しましょう。

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

```elixir
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`](https://hexdocs.pm/elixir/Kernel.html#if/2)に渡します。けれど、マクロが受け取るのは内部表現です(「[Elixirメタプログラミング 01: 内部表現 ー quote/2とunquote/1](https://dev.to/gumi/elixir---quote2unquote1-f4i-temp-slug-2512891?preview=aa737ae159320789d47880244c4d8f7932cf1d320b50728763c9f568b45c560772df16045eab26a2814ecb951abf68dc9c5df380e95ec4d0273989ea)」参照)。そして、それを差し込んだ別の内部表現を返します。

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

```
$ iex macros.exs
```

マクロを使うには、その前に[`require/2`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#require/2)でモジュールを要求しなければなりません。そのあと、関数と同じように呼び出せます。

```elixir
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`](https://hexdocs.pm/elixir/IO.html#puts/2)に渡した文字列が出力されません。文字列が関数で出力されたのは、値を返す前に引数が評価されるからです。これに対して、マクロは渡された引数を評価しません。引数は内部表現として受け取られ、別の内部表現にされるのです。今回、定義した`macro_unless`マクロは、`if`の内部表現になります。

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

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

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

```elixir
{: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`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#quote/2)で確かめられるでしょう。さらに、その内部表現を展開するのが[`Macro.expand_once/2`](https://hexdocs.pm/elixir/Macro.html#expand_once/2)です。

```elixir
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`](https://hexdocs.pm/elixir/IO.html#puts/2)で文字列に出力して確かめたということです。

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

```elixir
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`](https://hexdocs.pm/elixir/Kernel.html#unless/2)の実装はつぎのようなものです。

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

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

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


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

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

```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
```

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

```elixir
iex> HygieneTest.go
13
```

あえて、マクロが呼び出されたコンテキストに影響を与えたいときには、[`var!/2`](https://hexdocs.pm/elixir/Kernel.html#var!/2)を使ってください。

```elixir
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`に与えた変数は、呼び出されたコンテキストに上書きして定義されます。

```elixir
iex> HygieneTest.go
1
```

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

```
warning: variable "a" is unused
```

変数のコンテキストは、内部表現の第3要素のアトムで示されます。そして、モジュールから`quote/2`で引用された変数は、そのモジュールをコンテキストにもつのです。そのため、他のコンテキストを汚すことなく健全さが保たれます(「[健全なマクロ](https://ja.wikipedia.org/wiki/%E5%81%A5%E5%85%A8%E3%81%AA%E3%83%9E%E3%82%AF%E3%83%AD)」参照)。

```elixir
defmodule Sample do
  def quoted do
    quote do: x
  end
end
```

```elixir
iex> quote do: x
{:x, [], Elixir}
iex> Sample.quoted
{:x, [], Sample}
```

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

[`Macro.var/2`](https://hexdocs.pm/elixir/Macro.html#var/2)を使うと、動的に変数をつくることができます。第1引数が変数名で、第2引数はコンテキストです。

```elixir
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
```

```elixir
iex> Sample.run
[3, 5, 6]
```


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

```elixir
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](https://hexdocs.pm/elixir/Macro.html)」をご参照ください。また、コンパイル環境については「[Macro.Env](https://hexdocs.pm/elixir/Macro.Env.html)」で解説されています。


# プライベートマクロ
Elixirは[`defmacrop/2`](https://hexdocs.pm/elixir/Kernel.html#defmacrop/2)で、プライベートマクロが定義されます。プライベートな関数になりますので、マクロを定義したモジュールの中で、コンパイル時にしか使えません。

```elixir
defmodule Sample do
  defmacrop two, do: 2
  def four, do: two + two
end
```

```elixir
iex> Sample.four
4
```

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

```elixir
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のマクロは、呼び出す側がコンパイルのとき明示的に実行しなければならないのです。
- **明確**: 多くの言語には`quote`や`unquote`に省略記法が備えられています。Elixirではフルに入力することにしました。マクロ定義と内部表現をはっきりと識別できるようにするためです。

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

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

```elixir
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を設計するのにも役立つでしょう。

```elixir
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
```