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

Elixir: Macro入門

More than 3 years have passed since last update.

概要

Elixir の Macro の基礎的なことをまとめました。

動機

Ecto.Query.select を使って次のようなコードを書いたところエラーが発生しました。

ecto_select.ex
RssFeed
|> select([f], %{id: f.id})
error
** (CompileError) ecto_select.ex:19: function f/0 undefined

たしかに、f を定義していないですが、ドキュメント のとおりで、前に同じように書いたときは動いていました。

Ecto のバージョンを確認したり、いろいろ調べた結果、import Ecto.Queryを忘れていただけという単純なミスでした。
しかし、定義されていない f が使えていることが理解できていないことに気づき調べました。

マクロ

Ecto.Query.select のコードを確認するとマクロであることが分かります。
使う側からすると見た目が似ている関数とマクロですが、実際は渡ってくるパラメータや戻り値の意味が大きく異なります。

同じものをマクロと関数で実装し確認します。

sampe1.exs
defmodule Sample do
  defmacro sample_macro(param) do
    IO.puts "### sample_macro ###"
    IO.inspect param
    ""
  end

  def sample_func(param) do
    IO.puts "### sample_func ###"
    IO.inspect param
    ""
  end
end

defmodule Main do
  require Sample
  import Sample

  def main() do
    x = 1
    sample_macro(x > 0)
    sample_func(x > 0)
  end
end

Main.main

結果
### sample_macro ###
{:>, [line: 21], [{:x, [line: 21], nil}, 0]}
### sample_func ###
true

関数では x > 0 が実行されたあと、関数に渡されるので paramtrue になります。関数に渡されるタイミングで実行される必要があるため、x = 0 を消すと function x/0 undefined になります。
こちらは普段良く使っているため、理解しやすいと思います。

マクロの方は {:>, [line: 21], [{:x, [line: 21], nil}, 0]} という x > 0 という構文が解析された結果が渡ります。
この形式を AST (abstract syntax tree、抽象構文木) と言います。
構文が渡るだけなので、x = 0 を消してもこちらはエラーになりません。

次に少し修正して、受け取った param を返します。

sample1-1.ex
defmodule Sample do
  defmacro sample_macro(param) do
    IO.puts "### sample_macro ###"
    IO.inspect param
    param
  end

  def sample_func(param) do
    IO.puts "### sample_func ###"
    IO.inspect param
    param
  end
end

defmodule Main do
  require Sample
  import Sample

  def main() do
    x = 1
    IO.puts "result = #{sample_macro(x > 0)}"
    IO.puts "result = #{sample_func(x > 0)}"
  end
end

Main.main
結果
### sample_macro ###
{:>, [line: 21], [{:x, [line: 21], nil}, 0]}
result = true
### sample_func ###
true
result = true

両方とも結果は true になりました。 関数の paramtrue なので、true を返すと true になることは分かります。

マクロの param は AST です。{:>, [line: 21], [{:x, [line: 21], nil}, 0]} という AST を返して true が得られるということから、マクロでは AST を返し それを実行した結果 が呼び出し元に返されることが分かります。

関数を実行する AST

{:関数名, [], nil} で関数が呼べます。

こちらのサンプルのように、Sample モジュールは Main モジュールの hello にはアクセスしていませんが、hello を呼ぶ AST を返すことで呼び出し元の hello を実行できます。

sample2.exs
defmodule Sample do
  defmacro sample() do
    {:hello, [], nil}
  end
end

defmodule Main do
  require Sample
  import Sample

  def hello() do
   IO.inspect "hello"
  end

  def main() do
    sample()
  end
end

Main.main

呼び出し元に関数を実装しておいて、モジュールからはその関数を呼ぶ AST を返すことでコールバックのようなこともできます。(意図せず呼ばれてしまうと分かりやすいとは言えないですが、、、)

AST については ドキュメント を参照してください。 quote などを使うことで確認できます。

DSL

実行した結果ではなく構文が渡るので、実行できなくても構文的に正しければ、マクロのパラメータとして渡すことができます。

sampe3.exs
defmodule Sample do
  defmacro sample(args) do
    IO.inspect args
    ""
  end
end

defmodule Main do
  require Sample
  import Sample

  def main() do
    sample(this is a pen)
    sample(this is a <b>new</b> pen)
  end
end

IO.inspect Main.main

上の例のように、Elixir の構文として正しければ、マクロに渡せます。
逆に構文として正しくない、this is a pen. (ドットがついている)や、this is a <i><b>new</b></i> pen><という構文がない)はエラーになります。

この仕組みを使って、(構文が Elixir であれば)独自の言語を作ることができます。これを DSL (domain-specific language、ドメイン固有言語) と言います。

誰が見ても分かりやすい DSL を作ることで生産性や可読性が高まりますが、Elixir 本来の動作と変わるため、理解を難しくすることもあります。むやみに独自 DSL 作るのは避けた方が良さそうです。

何でもできる

マクロを使えば呼び出し元で実行する構文を返せるので、多分、何でもできると思います。
例えば、呼んだだけで呼び出し元の変数を変更するマクロもできます。

こちらの例では、updatex が呼ばれると x が勝手に 3 にセットされてしまいます。

sample4.exs
defmodule Sample do
  defmacro updatex() do
    {:=, [], [{:x, [], nil}, 3]}
  end
end

defmodule Main do
  require Sample
  import Sample

  def main() do
    x = 0
    IO.puts "(before) x=#{x}"
    updatex()
    IO.puts "(after) x=#{x}"
  end
end

IO.inspect Main.main

予測できないところで、変数の変更や関数の呼び出しが起こると混乱の元になるので、使い方に注意が必要です。

最後に

マクロはとても強力な機能ですが、よく言われるように「大いなる力には、大いなる責任が伴う」 ということを忘れないようにしていきたいです。

参考

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