Elixirのマクロについて浅く語ってみる
1.0のリリースが近づき、Elixir熱の高まりを感じる今日この頃、皆様いかがお過ごしでしょうか。
本日は、Elixirの特徴の1つであるマクロについて書いておこうと思います。対象読者は、Elixirの文法くらいはなんとなく分かるっていう人かな。
コードを書くコード
公式の解説をなぞって、unless
マクロを見てみます。
defmodule Unless do defmacro macro_unless(clause, expression) do quote do if(!unquote(clause), do: unquote(expression)) end end end require Unless Unless.macro_unless true, IO.puts("Hello") #=> 何も出力されない
Unless
モジュールにmacro_unless
を定義しました。これにtrue
とIO.puts("Hello")
を渡すと、
macro_unless(true, {{:., [], [IO, :puts]}, [], ["Hello"]})
マクロ側ではこんなものになって渡ってきます(一部省略しています。公式の解説は中カッコの位置がおかしい気がする)。Elixirのコードは、内部的には3つの要素を持つタプルの入れ子で表現されるので、この形になって渡ってくるのです。んで、結果的に、
{:if, [], [ {:!, [], [true]}, {{:., [], [IO, :puts]}, [], ["Hello"]} ]}
これを返すと。つまりどういうことかというと、
Unless.macro_unless true, IO.puts("Hello")
こう書くのは、
if(!true), do: IO.puts("Hello")
こう書いたのと等価っていうこと。もっと言うと、コードが変換されて置き換えられたということ。
quote
とunquote
quote
は、与えられたコードをASTに変換します(Elixirではquoted expressionと呼ぶそうですが便宜上ASTと書きます)。これを使わずに、自力で{:if, [], [...]}
のようなASTを組み上げてもマクロは動作しますが、苦行なのでquote
しましょう。
unquote
は、quote
されるコードの中にASTをイイ感じに組み込んでくれます。直感的には、unquote
内のASTのコード表現が、そのままそこに埋め込まれる感じです。上の例で、仮にunquote
せずにそのまま
quote do if(!clause, do: expression) end
と書いてしまうと、呼び出し側でclause
とexpression
を解決しようとして、そんな関数ありませんというエラーが出てしまいます。ちなみに、呼び出し側でclause
, expression
という関数なり変数を用意しておいても、スコープが共有されないので、同じエラーがでます。
もっと見る
プログラミングに限らず学習全般に言えることですが、自分で手を動かしたり、お手本になる何かをじっくり見たりするのは大事なことです。なのでElixirのソース読みましょう。ただしいきなり飛び込むと深すぎて溺死するので浅瀬から。
まずは本物のunless
。do
とelse
を入れ替えたif
ですね。
defmacro unless(clause, options) do do_clause = Keyword.get(options, :do, nil) else_clause = Keyword.get(options, :else, nil) quote do if(unquote(clause), do: unquote(else_clause), else: unquote(do_clause)) end end
次にif
。こいつもマクロ。"条件部分を評価した結果がfalse
またはnil
だったらelse
句を、そうでなかったらdo
句を返すcase
"のASTを返しています。optimize_boolean
は一旦無視。(オイ
defmacro if(condition, clauses) do do_clause = Keyword.get(clauses, :do, nil) else_clause = Keyword.get(clauses, :else, nil) optimize_boolean(quote do case unquote(condition) do x when x in [false, nil] -> unquote(else_clause) _ -> unquote(do_clause) end end) end
例外を投げるraise
もマクロ。
defmacro raise(exception, attrs) do quote do :erlang.error unquote(exception).exception(unquote(attrs)) end end
# これが raise ArgumentError, message: "oops" # こうなる :erlang.error ArgumentError.exception(message: "oops")
第1引数のモジュールのexception/1
を呼ぶコードに変換されています。
まとめ
長くなったのでこの辺で〆ます。マクロを使うと、"コードを書くコード"が書けます。いわゆるメタプログラミングです。DSL定義などなど、用途は様々。
最後に、英語ですが素晴らしい学習資料を紹介しておきます。
もっと日本語の資料が充実してくれると嬉しいですね。1.0が出たら段々と増えるでしょうか。