概要
Elixir の Macro の基礎的なことをまとめました。
動機
Ecto.Query.select
を使って次のようなコードを書いたところエラーが発生しました。
RssFeed
|> select([f], %{id: f.id})
** (CompileError) ecto_select.ex:19: function f/0 undefined
たしかに、f
を定義していないですが、ドキュメント のとおりで、前に同じように書いたときは動いていました。
Ecto のバージョンを確認したり、いろいろ調べた結果、import Ecto.Query
を忘れていただけという単純なミスでした。
しかし、定義されていない f
が使えていることが理解できていないことに気づき調べました。
マクロ
Ecto.Query.select
のコードを確認するとマクロであることが分かります。
使う側からすると見た目が似ている関数とマクロですが、実際は渡ってくるパラメータや戻り値の意味が大きく異なります。
同じものをマクロと関数で実装し確認します。
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
が実行されたあと、関数に渡されるので param
は true
になります。関数に渡されるタイミングで実行される必要があるため、x = 0
を消すと function x/0 undefined
になります。
こちらは普段良く使っているため、理解しやすいと思います。
マクロの方は {:>, [line: 21], [{:x, [line: 21], nil}, 0]}
という x > 0
という構文が解析された結果が渡ります。
この形式を AST (abstract syntax tree、抽象構文木) と言います。
構文が渡るだけなので、x = 0
を消してもこちらはエラーになりません。
次に少し修正して、受け取った param
を返します。
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
になりました。 関数の param
は true
なので、true
を返すと true
になることは分かります。
マクロの param
は AST です。{:>, [line: 21], [{:x, [line: 21], nil}, 0]}
という AST を返して true
が得られるということから、マクロでは AST を返し それを実行した結果 が呼び出し元に返されることが分かります。
関数を実行する AST
{:関数名, [], nil}
で関数が呼べます。
こちらのサンプルのように、Sample
モジュールは Main
モジュールの hello
にはアクセスしていませんが、hello
を呼ぶ AST を返すことで呼び出し元の hello
を実行できます。
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
実行した結果ではなく構文が渡るので、実行できなくても構文的に正しければ、マクロのパラメータとして渡すことができます。
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
にセットされてしまいます。
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
予測できないところで、変数の変更や関数の呼び出しが起こると混乱の元になるので、使い方に注意が必要です。
最後に
マクロはとても強力な機能ですが、よく言われるように「大いなる力には、大いなる責任が伴う」 ということを忘れないようにしていきたいです。