QLC for elixir

QLCとはerlangのリスト内包表記を使ったクエリ書式であり、リストやEts, Dets, Mnesiaなどに対応している。今回はこれをelixirで使えるようにしようと思う。

qlcの使い方

基本的には、erlangなので変数は大文字で、

[Expression || Qualifier1, Qualifier2, ...]

Expression :: 任意のErlang式(template)

Qualifier :: Filter or Generators

Fiilter :: bool値を返すErlangGenerator :: Pattern <- ListExpression

ListExpression :: Qlc_handle or list()

わかりにくいかもしれないので、例を見てみる。

1> QH = qlc:q([{X,Y} || X <- [a,b], Y <- [1,2]]),
qlc:eval(QH).
[{a,1},{a,2},{b,1},{b,2}]

[a, b]と[1,2]の直積を計算している。
数学の集合の定義のように記述できるのが魅力である。
さて、これをelixirで実現するのだが、幸いqlc:string_to_handle/3というものがあるのでこれを使ってもよい。マニュアルにも、
'This function is probably useful mostly when called from outside of Erlang, for instance from a driver written in C.'
のように書いてある。しかし、erlangと同じbeam上に構築されたelixirがCと同様の土俵まで降りていくのは残念である。

:erl_scan.string/1, :erl_parse.parse_exprs/1

erlangには当然だが、スキャナとパーサが内蔵されている。これを使ってマクロにすることで、elixirのコンパイル時に文字列をerlang式としてパースすることができる。
elixirとちがいerlangの文字列は文字リストであることに注意すると、

  def exprs(s) do
    c = String.to_char_list(s)
    {:ok, m, _} = :erl_scan.string(c)
    {:ok, [expr]} = :erl_parse.parse_exprs(m)
    expr
  end

で文字列をErlangのASTにすることができる。これを実行時に:qlc.transform_expression/2で変数をバインドすることでqlc_handleを作ることができる。

Macro.escape/1 の使い所

通常マクロはelixir ASTをハンドリングするのだが、今回はErlang ASTをhandleするので、ただのタプルとして取り扱って欲しい。そのためにMacro.escape/1
を使う。

quote はbind_quotedする

マクロ中ではいつものquoteを使うのだが、引数をバインドするためには unquoteよりもbind_quoted: [keyword]を使うのが主流だ。

    quote bind_quoted: [exprl: exprl, bindings: bindings, opt: opt] do
      Qlc.expr_to_handle(exprl, bindings, opt)
    end

unquoteがないため実に見やすい。それだけではなく、評価の回数が保証されるため安全である。

これらを合わせてQlcマクロを作成すると、このようにかけることになる。

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

ソースはqlcに置いてみた。