アナフォリックマクロを作ってみる

Elixir Advent Calendar 2014 16日目。

またマクロのネタを。
On Lispでは、Common LISPの力の例として、アナフォリックマクロが紹介されている。elixirでも、アナフォリックマクロを作ってみよう。

アナフォリックマクロとは

アナフォラ(anaphora)というのは、既出の語をさす事で代名詞の事。アナフォリックマクロというのは、(決まった)代名詞(つまりアナフォラ)で既に評価された値を参照して利用できるマクロである。
文章で書くと分かりにくいが、sedでいうと、fooをfoobarに置換するときのs/foo/&bar/における'&'だし、オブジェクト指向的言語()のselfやthisなどもそうだ。perlはアナフォラが活躍しており、$_は至る所で様々なものを参照することになっている*1

初めてのアナフォラ

さて、どんなものをアナフォラにすると嬉しいだろうか。elixirはnilとfalse以外は真偽値としてはtrue扱いになるため、ifでの条件値をdoやelseでitとかいう変数名で参照出来ると嬉しいかもしれない*2。名前はanaphoric ifの頭文字でaifとする*3。つまり、こんな感じにつかえることになる。

iex(6)> it =2
2
iex(7)> aif (length [1,2,3,4]) do
...(7)>   IO.inspect it  ## <== ここで itという変数でaifの条件値を参照
...(7)> end
4
4
iex(8)> it ## itの値は外のコンテキストに影響与えない
               ## 健全(Hygiene)ですね!!
2

次はこういうマクロを作ってみよう。

マクロの設計

形は決まったので、いつものように、どう展開してほしいかを検討してみる。下記の例をどう展開してほしいかというと、

aif (length [1,2,3,4]) do
  IO.inspect it
end

これを、

if (it = (length [1,2,3,4])) do
  IO.inspect it
end

としてほしい。
なので、Aifというモジュールにaifマクロを記述できるような気がするのでやってみる。

defmodule Aif do
  defmacro aif(e, block) do
    quote do
      if (it = unquote(e), block)
    end
  end
end

ちょっとわざとらしいが、コンパイルして使ってみよう。

iex(1)> c("lib/aif.ex")
[Aif]
iex(2)> import Aif
nil
iex(3)> aif (length [1,2,3,4]) do
...(3)>   IO.inspect it
...(3)> end
** (RuntimeError) undefined function: it/0
    
iex(3)> 

怒られた。実は、Elixirのマクロは健全(Hygiene)なので、マクロで導入した変数は外部のコンテキストへ影響を与えることは出来ない。blockの中身のコンテキストはマクロの外側なので、IO.inspect itは外部のコンテキストのitを探しにいってしまっていたのだ。
この場合、解決策は3つある。

  1. quote中でvar!/1を使い、外部コンテキストの変数とする。この場合、itの値が外側に漏れだしてしまう。以下で説明する。
  2. block中の変数を強制的にマクロの内部のコンテキストにする。Elixirではこれで行ってみよう。
  3. aifを無名関数とその実行(ようはクロージャ)として実装する。これでもかまわないが、遅いのと、block中で他の変数の(再)束縛ができなくなってしまう。これはかなり痛いので却下。

まず1は、

defmodule Aif do
  defmacro aif(e, block) do
    quote do
      if (var!(it) = unquote(e), block)
    end
  end
end

とすればいい。これだとitは呼び出し側コンテキストの変数として扱われる。

iex(5)> it = 0
0
iex(6)> aif (length [1,2,3,4]) do
...(6)>   IO.inspect it
...(6)> end
4
4
iex(7)> it
4

aifの実行後、0だったitが4に再束縛されていることが分かる。これでは怖くて使えない。そこで2の方法を使う。

コードウォーク

block中の変数(のコンテキスト)を書き換えるためには、コードウォークが必要だが、Elixirには幸いにもMacro.prewalk/2があるのでこれを使ってみよう。今回は、itという変数 {:it, META, CONTEXT} (ただしCONTEXTはアトム)について、{:it, META, Aif} (Aifはaifが定義されているモジュール)としてAifコンテキストに指定してみる。

defmodule Aif do
  defmacro aif(e, block) do
    b2 = Macro.prewalk(block,
                       fn(x) ->
                         case x do
                           {:it, m, ctx} when is_atom(ctx) ->
                             {:it, m, Aif}
                           x ->
                             x
                         end
                       end)
    quote do
      if(it = unquote(e), unquote(b2))
    end
  end
end

これでコンパイル、実行してみよう。

iex(5)> c("lib/aif.ex")
lib/aif.ex:1: warning: redefining module Aif
[Aif]
iex(6)> import Aif
nil
iex(7)> it = 0
0
iex(8)> aif (length [1,2,3,4]) do
...(8)>   IO.inspect it
...(8)> end
4
4
iex(9)> it
0
iex(10)> 

うまくいった。aifブロックの内部のみitがlength [1,2,3,4]に束縛されていることが分かる。しかし、aifをネストするとどうだろう。

iex(10)> aif (length [1,2,3,4]) do
...(10)>   aif (length [1,2,3]) do
...(10)>     IO.inspect [it: it]
...(10)>   end
...(10)>   IO.inspect [it4: it]
...(10)> end
[it: 4]
[it4: 4]
[it4: 4]

最初のaifの展開時に、内側のaifに属するitも展開してしまっているため、itが4になってしまっている。これを防ぐためには、コードウォークを工夫する必要がある。自分でコードウォークを書く方法もあるが、今回は、コードウォーク中にaif/2を見つけたら、Macro.expand_once/2を呼び出して、そこの部分から強制的にマクロ展開してしまうという方法を使おう。この方法は、マクロ定義の中で自分自身の展開を呼び出すというちょっと不思議なことになる。

defmodule Aif do
  defmacro aif(e, block) do
    b2 = Macro.prewalk(block,
                       fn(x) ->
                         case x do
                           {:aif, m, ctx} when is_list(ctx) ->
                             Macro.expand_once({:aif, m, ctx}, __ENV__)
                           {:it, m, ctx} when is_atom(ctx) ->
                             {:it, m, Aif}
                           x ->
                             x
                         end
                       end)
    quote do
      if(it = unquote(e), unquote(b2))
    end
  end
end

さてこれを使ってみよう。

iex(26)> aif (length [1,2,3,4]) do
...(26)>   aif (length [1,2,3]) do
...(26)>     IO.inspect [it: it]
...(26)>   end
...(26)>   IO.inspect [it: it]
...(26)> end
[it: 3]
[it: 4]
[it: 4]

うまく捕捉できているようだ。
このマクロを使っての制限は、通常の変数としてitを使う事が出来なくなる点である。aifの内部で束縛され、外部に漏れる事はない。どうしても、外部へ持っていきたいときは、doブロック中でvar!/1を使う。

iex(31)> it = 0
0
iex(32)> aif (length [1,2,3,4]) do
...(32)>   var!(it) = it
...(32)> end
4
iex(33)> it
4
iex(34)> 

参照透明性?

アナフォラは参照透明性を侵している。しかし、__ENV__や__MODULE__、__CALLER__はどうだろう。便利に使っているが、これも現在のコンテキストを参照できる、一種のアナフォラである。アナフォラは参照透明性を侵しているため嫌う人もいるが、アナフォラそのものが悪ではなく、混乱が悪なのである。参照透明性は、混乱を防ぐためのポリシーに過ぎない。そもそも、オブジェクトが不変であるElixirやErlangにとっては、変数そのものも値につけたラベルにすぎず、アナフォラのようなものだ。
aifの場合、ただ一つの変数を作成するだけであり、その変数の作成こそがaifマクロの存在理由であるため、混乱することはないだろう*4

ということで、アナフォリックマクロを作る話でした。
明日は、@Joe-nohさんです!。

*1:省略される事も多いが

*2:もっともelseで参照してもnilかfalseなのであまり嬉しくないかもしれない

*3:On Lispに倣ったというのが本当のところ

*4:この言い回しはOn Lispの受け売りです