10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Elixir:わかったつもりになれるかもしれないマクロ入門

Posted at

はじめに

今回の内容も, この本公式ドキュメントの内容の抜粋です.
もっと詳しく知りたい場合は, 直接そちらを読んだほうが良いです.

また, 以下の文章では「評価」と「実行」が雑然と使われていますが, 明確な考えのもと使い分けているわけではないので, よくわからない場合は全部「実行」に読み替えてください.
(無論, 「評価」と「実行」は別の意味を持っているとは思うのですが)

マクロとはなにか?

マクロとは, Elixirに新たな構文を定義し, 機能拡張するために仕組みである.

マクロの使い方

マクロの構文

Elixirでは以下の構文でマクロを定義することができます.

defmacro hoge(parameters) do
  statement
end

一見, 関数の定義と同じですね.

実際に使ってみる

公式のドキュメントにならって,

  MyMacro.unless false, do: something()

こんな感じに使えるunlessの実装を考えてみます.
コードは公式にかかれていたものをそのまま使って以下のとおり

defmodule MyMacro do
  def fun_unless(clause, expression) do
    if(!clause, do: expression)
  end

  defmacro macro_unless(clause, expression) do
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end
end

fun_unlessは関数による実装であり, macro_unlessはマクロによる実装となっています.
それぞれを実際に実行してみると

iex(49)> require MyMacro
require MyMacro
nil
iex(50)> MyMacro.macro_unless true, IO.puts "this should never be printed"
MyMacro.macro_unless true, IO.puts "this should never be printed"
nil
iex(51)> MyMacro.fun_unless true, IO.puts "this should never be printed"
MyMacro.fun_unless true, IO.puts "this should never be printed"
this should never be printed # <-IO.putsが実行されてしまっている
nil

ご覧のとおり, マクロで定義されたunlessは想定通り何も実行していないのにもかかわらず,
関数で定義されたunlessは引数のIO.putsが実行されてしまっています.

なぜ関数で定義したunlessは, 条件を満たしていないのにもかかわらずIO.putsが実行されてしまったのか?
これは他の関数呼び出しを思い出してみればすぐわかると思うのですが, Elixirでは関数を呼び出した場合, まず各引数が評価され, その結果が関数に渡されます.

この例だと, まず引数のIO.puts "this should never be printed"が評価され

iex(53)> IO.puts "this should never be printed"
IO.puts "this should never be printed"
this should never be printed
:ok

その返り値である:okが関数fun_unlessに渡されることになるのです.
つまり

  MyMacro.fun_unless true, IO.puts "this should never be printed"

という呼び出しは, 結局のところ

  ret = IO.puts "this should never be printed"
  MyMacro.fun_unless true, ret

と同じということです.

なので, この条件を満たしていなのに"this should never be printed"が出力されてしまうという挙動は,
不思議でも何でもなく当然の挙動なのです.

では, 逆に考えると, マクロではなぜ引数のIO.putsが評価されなかったのでしょうか?

マクロには何が渡され, そして何を返しているのか?

マクロに何が渡されているかを確認するために, 以下のようなマクロを定義して実行してみます.

  defmacro inspect_and_do(code) do
    IO.inspect code
    code
  end

実行結果は以下のとおり

iex(55)> MyMacro.inspect_and_do IO.puts("Hello")
MyMacro.inspect_and_do IO.puts("Hello")
{{:., [line: 55], [{:__aliases__, [counter: 0, line: 55], [:IO]}, :puts]},
 [line: 55], ["Hello"]}
Hello
:ok

突然出てきた、この

{{:., [line: 55], [{:__aliases__, [counter: 0, line: 55], [:IO]}, :puts]},
 [line: 55], ["Hello"]}

とは何者でしょうか?
これはElixirのパーサがソースを解析してつくり上げる内部構造そのものです.
(以下AST(Abstract Syntax Tree:抽象構文木)と呼びます. Elixirではまた別の名前がつけられているようですが…)

これで関数とマクロは,

  • 関数は値を受け取り, 何らかの処理を行い, 値を返す
  • マクロはASTを受け取り, 何らかの処理を行い, ASTを返す(そして, マクロから抜けた時点でASTは評価される)
    と全くの別物であることがわかったと思います.

quote, unquote

さて, ここまでの説明を読んで, 「ASTを返すのはいいけどどうやってAST構築するの?まさか、こんなカギ括弧と中括弧だらけの構文を手動で書けとか言わないよね?」と思った方もいらっしゃるかもしれません.
ご心配はいりません.
Elixirでは, 通常の記述をASTに変換するために, quote1という関数が用意されています.
使い方も簡単.

  defmacro goodbye() do
    code = quote do
      IO.puts "Goodbye"
    end

    IO.inspect code

    code
  end

このように, ブロック文をquote関数の引数として渡すだけで, ブロックの中身がASTに変換されて得られます.

iex(59)> MyMacro.goodbye
MyMacro.goodbye
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["Goodbye"]}
Goodbye
:ok

ここで注意が必要なのは, 最初のunlessを実装しようと

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

このように書くと

iex(61)> MyMacro.macro_unless2 true, "this should never be printed"
MyMacro.macro_unless2 true, "this should never be printed"
** (CompileError) iex:61: undefined function clause/0
    (elixir) expanding macro: Kernel.!/1
             iex:61: (file)
    (elixir) expanding macro: Kernel.if/2
             iex:61: (file)
             expanding macro: MyMacro.macro_unless2/2
             iex:61: (file)

このようにエラーになってしまいます.
これを避けるにはunquote関数を使って

  defmacro macro_unless(clause, expression) do
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end

と書いてやる必要があります.

なぜunquote関数を使う必要があるのか?
これは, quote関数の中と外でスコープを共有していないから, とか, quote内部では独立して構文のパースしようとしているため, quote内部で定義されていない変数へのアクセスはできないので, quote内部から外部の変数にアクセスするときは一旦パースを止めてやる必要があるため, とか色々説明は読んだのですが, 実は私も完全には理解できていません.

ネタ本には
「どうしても理解できないなら, 文字列の補間(interpolation)をする際に"same #{varibles}"と変数を#{}で囲って書くように, quote内で変数を使いたいときはunquoteで囲うとだけ覚えておけ(意訳)」
とやや割りきったことが書かれていたので, これに従うことにしています.

再び、マクロとはなにか?

マクロとは, Elixirの評価前の内部構造にアクセスし, 変更を加えるための仕組みである.

つまり, 単に文字列の置き換えを行うC言語のマクロではなく, ASTの操作を行うLispのマクロと同じものである.(多分)

なので, マクロについてもっと知りたい場合は, ひょっとしたらLisp関係の情報に当たるのもいいのかもしれません.

おわりに

(Lispの意味での)マクロは、私の知る限り依然としてLispに特有のものだ。 たぶん、マクロを持つためには言語をLispと同じような奇妙な外見にしないと 駄目だからだろう。それにまた、マクロという最後の力を加えたら、 それは新しい言語ではなくLispの新しい方言になってしまうからだろう。

                   ポール・グラハム - 技術野郎の復讐

参考

Macros: http://elixir-ja.sena-net.works/getting_started/meta/2.html

Programming Elixir: Functional, Concurrent, Pragmatic, Fun: http://www.amazon.co.jp/Programming-Elixir-Functional-Concurrent-Pragmatic/dp/1937785580

技術野郎の復讐: http://practical-scheme.net/trans/icad-j.html

  1. なぜこの関数にquoteなんて名前が付いている理由は, おそらくLispで関数の引数として評価していないS式そのものを渡したい場合, S式の頭に「'」(quote)をつけていることからの連想だと思われます

10
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?