マクロでerlang:qlcモジュールの妙な制限を撤廃する

Qlcモジュール

erlangにはリスト内包表記でクエリを記述できるqlcモジュールというものが標準で付いており、それをelixirから利用するためのモジュールがQlcだ。hex.pmに登録してあるのでmix.exsにdepsするだけで利用できる。インストールしたのちの話だが、以下のように使うことができる。

iex> require Qlc
iex> list = [a: 1, b: 2]
iex> "[X || X = {K,V} <- L, K =/= Item]"  |>
iex>   Qlc.q(L: list, K: :b) |>
iex>   Qlc.e()
[a: 1]

ところがこうはできない。

iex> list = [a: 1, b: 2]
iex> query = "[X || X = {K,V} <- L, K =/= Item]"
iex> query |> Qlc.q(L: list, K: :b) |> Qlc.e()
** (FunctionClauseError) no function clause matching in Qlc."MACRO-q"/4    
....

なぜかというと、:qlc.q/2の第一引数はリテラルerlang文字列でなければならないという制限があるためだ。それを知らない人から

github.com

というProbrem reportが来た。全部変数にしたいらしい。たしかに。とりあえずベースのerlang:qlcの制限なのだと回答したのだが、コンパイル時に変数かリテラルかは判定できるので、マクロで解決できることに気が付いた。

いつもの変態ハック

Qlc.qがしていたことは、

  • elixirでの文字列をerlangでの文字リストへ変換すること
  • 末尾にピリオドがない場合には追加すること(erlangのリスト内包表記の構文に合わせるため)
  • :qlc.string_to_query/3を呼び出す

という一連のことをコンパイル時に行っていた。これに追加して、

  • 第一引数がリテラルでない場合は、上記3点を「実行時」に行うことにすればいい。

変数や関数呼び出しなら:qlc.string_to_query/3を実行時に呼び出すのはしょうがないし、それらの判断を実行時ではなくコンパイル時に行うことで、実行時のオーバヘッドは0になる。 これらを行うために

  defmacro q(string, bindings, opt \\ []) do
    case is_binary(string) do
      true ->
        exprl = (String.ends_with?(string, ".") && string || string <> ".")
          |> exprs()
          |> Macro.escape()
        quote bind_quoted: [exprl: exprl, bindings: bindings, opt: opt] do
          Qlc.expr_to_handle(exprl, bindings, opt)
        end
      false ->
        quote bind_quoted: [string: string, bindings: bindings, opt: opt] do
          Qlc.string_to_handle(string, bindings, opt)
        end
    end
  end

としただけ。こういうリテラルでなくてはならないとかいう変な制限を撤廃できるのもマクロの良さというわけだ。

現在elixirコミュニティでは「マクロは使うな」というのがコモンセンスなのだが、マクロがないelixirはマクロがないlispと同じだ。つまり意味がない。マクロこそがelixirの力の源泉なのだ。ただ、力が強大すぎるため注意するべきだし、乱用するべきではないのはその通りだ。 たとえば、Cのポインタやmalloc/callocといった機能を使うなというのは、初心者への忠告に過ぎず、Cを本気で武器にするならCの機能を必要に応じて全て使うべきである。

それと同様にalchemistとしてapprenticeからjourneymanへステップアップしたいならば、マクロの利用をためらってはならない。