実行時にマクロ展開を行う
この記事は #サッポロビーム #137 でやったことの記事です。
クリス・デイトのリレーショナル言語「tutorialD」が気に入ったので、処理系を作ろうと思っていた。
erlangのmnesiaを基盤として、とりあえずできたのだが、何をするにもフルスキャン必須で、実用にならないため、実装方法を再検討することにした。qlcは、リスト内包表記のコンパイル時に、使える場合はインデックスを使うようにクエリを最適化してくれるので、その機能を使いたい。
例:
{table2, key, mark, value}
というタプルの場合で、value列にはindexを作成していると仮定して、valueを検索したいとしよう。タプルXのvalue要素はelement(4, X)で取り出せるので、インデックスを使って欲しい場合、qlcのクエリとしては
qlc:q("[ X || X <- Q, element(4, X) =:= Y]", [{Q, mnesia:table(table2)}, {Y, x}])
のようにするとよい。しかしこの、element(4,X)の4というのはコンパイル時に決まっている必要があるが、データベースのスキーマはコンパイル時には予見できない。
でもプログラミング時にはvalueという名前で4番目の要素を指定したい。
こんな時にはマクロでなんとかできないかと考えるわけだが、今回は実行時に解決しなければならない。ということで、実行時にASTを展開して、一旦文字列化してからqlc:string_to_handle/3 を呼ぶ方針にする。
もう一つの問題
relvar |> where(value == 4 and mark == :atom1)
などと書くと、実行時に (value == 4 and mark == :atom1)を書き換えて、
:qlc.string_to_handle("[ X || X <- Q, element(4, X) =:= Y, element(3, X) =:= Z].", [{Q, mnesia:table(relvar.table()), {Y, 4}, {Z, :atom}])
のようにして欲しいわけだが以下の問題がある。
- =:=/2という中置演算子はelixirにはないのでMacro.to_string/2では :"=:="(a, b)となってしまう。
- 同様に,/2や;/2も,(a, b), ;(a, b)となってしまう。
実はMacro.to_string/2の第2引数を使うことで、文字列化の制御が可能だ。
def fmt(ast, x) do case ast do {:"==", m, [a, b]} -> Macro.to_string(a, &fmt/2) <> " =:= " <> Macro.to_string(b, &fmt/2) {:and, m, [a, b]} -> Macro.to_string(a, &fmt/2) <> ", " <> Macro.to_string(b, &fmt/2) {:or, m, [a, b]} -> Macro.to_string(a, &fmt/2) <> "; " <> Macro.to_string(b, &fmt/2) _ -> x end end
このようにすることで、必要な演算子の文字列化を制御できる。書き換えポイントで再帰的に
Macro.to_string/2を呼び出しているので、それまで構築していたものは捨てられてしまうが
しょうがない。
最後の問題
上記では、where句の引数は実行時まで評価されないため、elixirの変数を含んだ式を記述できない。
これではいくら何でも使いにくすぎる。そのため、
a = 4 b = :atom1 relvar |> where ([a: a, b: b], value == a and mark == b)
のようにして変数の束縛を指定することで、コンパイル時に
a = 4 b = :atom1 relvar |> where (quote bind_quoted: [a: a, b: b], do: value == a and mark == b)
のように変換し、実行時(つまりマクロ展開時)にa, bの値を決められるようにしておけばいい。
つまり「マクロを生成するマクロ」を作り、実行時に生成されたマクロを展開したうえでqlcが求めている形式にて:qlc.string_to_handle/3で最適化されたクエリを構築することになる。