mmag

ハマったことメモなど

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を定義しました。これにtrueIO.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")

こう書いたのと等価っていうこと。もっと言うと、コードが変換されて置き換えられたということ。

quoteunquote

quoteは、与えられたコードをASTに変換します(Elixirではquoted expressionと呼ぶそうですが便宜上ASTと書きます)。これを使わずに、自力で{:if, [], [...]}のようなASTを組み上げてもマクロは動作しますが、苦行なのでquoteしましょう。

unquoteは、quoteされるコードの中にASTをイイ感じに組み込んでくれます。直感的には、unquote内のASTのコード表現が、そのままそこに埋め込まれる感じです。上の例で、仮にunquoteせずにそのまま

quote do
  if(!clause, do: expression)
end

と書いてしまうと、呼び出し側でclauseexpressionを解決しようとして、そんな関数ありませんというエラーが出てしまいます。ちなみに、呼び出し側でclause, expressionという関数なり変数を用意しておいても、スコープが共有されないので、同じエラーがでます。

もっと見る

プログラミングに限らず学習全般に言えることですが、自分で手を動かしたり、お手本になる何かをじっくり見たりするのは大事なことです。なのでElixirのソース読みましょう。ただしいきなり飛び込むと深すぎて溺死するので浅瀬から。

まずは本物のunlessdoelseを入れ替えた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定義などなど、用途は様々。

最後に、英語ですが素晴らしい学習資料を紹介しておきます。

Understanding Elixir Macros

もっと日本語の資料が充実してくれると嬉しいですね。1.0が出たら段々と増えるでしょうか。