QLC for elixir
QLCとはerlangのリスト内包表記を使ったクエリ書式であり、リストやEts, Dets, Mnesiaなどに対応している。今回はこれをelixirで使えるようにしようと思う。
qlcの使い方
基本的には、erlangなので変数は大文字で、
[Expression || Qualifier1, Qualifier2, ...] Expression :: 任意のErlang式(template) Qualifier :: Filter or Generators Fiilter :: bool値を返すErlang式 Generator :: 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に置いてみた。
アナフォリックマクロを作ってみる
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つある。
- quote中でvar!/1を使い、外部コンテキストの変数とする。この場合、itの値が外側に漏れだしてしまう。以下で説明する。
- block中の変数を強制的にマクロの内部のコンテキストにする。Elixirではこれで行ってみよう。
- 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さんです!。
[elixir] マクロを使ってloop/recurを実装してみる
Elixir Advent Calendar 2014 7日目の記事。
前日は@keithseahusさんの 去年のやつをv1.0仕様で書き直します だった。
loop/recurとは何?
最近からelixirを始めた人にはピンとこないかもしれないが、elixir-0.5あたりでサポートされていた、無名関数を使わずにループを行う構文だ。clojureにインスパイアされたと言われていた。
その後、問題があったため、elixir-0.6あたりでdeprecatedとなり、現在は存在しない。
オリジナルのloop/recurの具体的な使いかたは、
loop initial-args do case1 -> do_1 case2 -> do_2 ... casen -> do_n recur next-args end
といったもので、例えば、
loop 10 do 0 -> 1 1 -> 1 x -> recur(x-1) + recur(x-2) end
のように使う*1。caseと非常に似ているが、SpecialFormだったため、loopの変数の数(アリティ)は、任意の個数が可能であった。
ここで、このなくなってしまったloop/recurのサブセットを復活させてみる。マクロで実装する都合上、loopの引数は1つだけ*2。それ以外は忠実に再現してみる事を目標にする。
方針
一般に、マクロを作る場合は、「どのように書きたいか」と、「どのように展開してほしいか」を考える事から始める。例として、フィボナッチ関数の定義をどうやって展開していくかを考える道筋を示しながら、検討していこう。
loop 10 do 0 -> 1 1 -> 1 x -> recur(x-1) + recur(x-2) end
これをどのように展開してほしいか。まずは無名関数とその呼び出しにしてほしいので、こんな感じか。
fn(x) -> case x do 0 -> 1 1 -> 1 x -> recur(x - 1) + recur(x - 2) end end.(10)
しかし、recur/1をどうするか。無名関数を定義した後で、その関数を引数にして呼び出せば、再帰できるので、そのポイントまでを書くと、
f = fn(y, x) -> case x do 0 -> 1 1 -> 1 x -> recur(x - 1) + recur(x - 2) end end f.(f, 10)
となる。これで無名関数の中でy.という名前で自分自身を参照できるようになった。一方、recurは名前付きの関数だが、これを渡された無名関数を使うようにして、ついでに、仮引数をyからrecurにしてみると*3、
recur = fn(recur, x) -> case x do 0 -> 1 1 -> 1 x -> recur.(recur, x - 1) + recur.(recur, x - 2) end end recur.(recur, 10)
できた。さて、これが実行できるかためしてみる。
iex(3)> recur = fn(recur, x) -> ...(3)> case x do ...(3)> 0 -> 1 ...(3)> 1 -> 1 ...(3)> x -> recur.(recur, x - 1) + recur.(recur, x - 2) ...(3)> end ...(3)> end #Function<12.90072148/2 in :erl_eval.expr/5> iex(4)> recur.(recur, 10) 89
うまく動いている感じなので、変換の内容をまとめると、
defmacro loop(a, block)は、以下をする:
- blockについては、内部を参照(コードウォーク)してrecur(x) をrecur.(recur, x)に変換する。
- 変換後のブロックmblockと初期値aを使い以下のようにする。
recur = fn(recur, x) -> case(x, mblock) end
recur.(recur, a)
こんな感じ。elixirのASTは、
{ 関数, META, ARGS }
{ 変数, META, CONTEXT }
となっていて、今回はrecurという関数をrecurという変数に変換することになる。また、変数に格納された無名関数を呼び出す関数は:"."で、
{{:".", META, [{変数, META, CONTEXT}]}, META, 引数}
となる。今回の変数は関数の引数なのでCONTEXTはnilとなる。
マクロの実際
変換そのものはコードウォークが必要だが、今回程度の変換は、手で書くことができる。関数fの戻値が{:skip, r}なら、それ以上の解析はせず、rを返し、{:done, ret}なら、retをさらに{op, meta, arg}に分解し、opとargをそれぞれ解析するというのが骨子。
def traverse(a, f) do {ret, r} = f.(a) case ret do :skip -> r :done -> cond do is_tuple(r) -> case r do {op,meta,arg} -> {traverse(op, f), meta, traverse(arg, f)} r -> List.to_tuple(traverse(Tuple.to_list(r), f)) end is_list(r) -> :lists.map(fn(x) -> traverse(x, f) end, r) true -> r end end end
変換は、このtraverse/2を使って
mb = traverse(block, fn(x) -> case x do {:recur, x, args} when is_atom(args) -> {:done, {:mrecur, x, nil}} {:loop, _meta, args} when is_list(args) -> {:skip, x} {:recur, meta, args} when is_list(args) -> {:done, {{:".", meta, [{:recur, meta, nil}]}, meta, [{:recur, meta, nil}|args]} } _ -> {:done, x} end end)
とすっきり記述できる。また、関数呼び出しは、
m = {:fn, [], [{:"->", [], [[{:recur, [], nil}, {:x, [], nil}], {:case, [], [{:x, [], nil}, mb]}]}]}
のように書ける。ここまで準備すると、recurの呼び出し部分の変換は、
quote do recur = unquote(m) recur.(recur, unquote(p)) end
となる。全体をまとめると、
defmodule MyMacro do @doc """ (block, {:mrecur,_, args}, {{:.,0,[{:f, 0, :quoted}]}) """ def traverse(a, f) do {ret, r} = f.(a) case ret do :skip -> r :done -> cond do is_tuple(r) -> case r do {op,line,arg} -> {traverse(op, f), line, traverse(arg, f)} r -> List.to_tuple(traverse(Tuple.to_list(r), f)) end is_list(r) -> Enum.map(r, &(traverse(&1, f))) true -> r end end end defmacro loop(p, block) do mb = traverse(block, fn(x) -> case x do {:recur, x, args} when is_atom(args) -> {:done, {:recur, x, nil}} {:loop, line, args} when is_list(args) -> {:skip, x} {:recur, line, args} when is_list(args) -> {:done, {{:".", line, [{:recur, line, nil}]}, line, [{:recur, line, nil}|args]} } _ -> {:done, x} end end) m = {:fn, [], [{:"->", [], [[{:recur, [], nil}, {:x, [], nil}], {:case, [], [{:x, [], nil}, mb]}]}]} quote do recur = unquote(m) recur.(recur, unquote(p)) end end end
使ってみる
上記のコードをMyMacro.exとでもいうファイルに保存して、importしてiexから使ってみる。
iex(11)> c("MyMacro.ex") [MyMacro] iex(12)> import MyMacro nil iex(13)> loop 10 do ...(13)> 0 -> 1 ...(13)> 1 -> 1 ...(13)> x -> recur(x-1) + recur(x-2) ...(13)> end 89
現在のelixirでも、ちょっとしたループを記述するのにはやっぱり便利である。
アッカーマン関数を定義してみる
アッカーマン関数とは、
def ack(0, n) -> n + 1 (m, 0) -> ack(m - 1, 1) (m, n) -> ack(m - 1, ack(m, n - 1) end
というもので、原始再帰関数*4ではない関数の一つ。
今回のloop/recurでは、これを定義、実行できる。
iex(12)> ack = fn(m, n) -> ...(12)> loop {m, n} do ...(12)> {0, n} -> n + 1 ...(12)> {m, 0} -> recur({m-1, 1}) ...(12)> {m, n} -> recur({m-1, recur({m, n-1})}) ...(12)> end ...(12)> end #Function<12.90072148/2 in :erl_eval.expr/5> iex(13)> ack.(3, 2) 29 iex(14)> ack.(3, 3) 61 iex(15)> ack.(3, 4) 125
このようにちょっとした再帰関数をアドホックに定義することも便利になる。
明日は@keithseahusさんです。
elixirプロトコルについて(Enumerable)
elixirでは標準でいくつかのプロトコルを提供しているが、あまり目立っていない。そこで、モジュールではなくプロトコルに着目して調べてみた。
Enumerable
Enumerableとは、「数え上げることができる」という意味で、主にEnum、StreamモジュールがEnumerableを対象とした操作を行っている。
Enumerableが実装を要求する関数は、count/1, member?/2, reduce/3の3個で、
それらを実装したモジュールはEnumerableになり、Enum, Streamモジュール
の恩恵をうけることができる。
とりあえず、フィボナッチ数を生成するモジュールFibを作ってみる。
まず、構造体と初期化子を作ります。これはプロトコルでは要請されていないが、お約束という事で。デフォルトでは無限に生成するようcountは:infinityにしてみる。
defmodule Fib do defstruct f0: 1, f1: 1, count: :infinity def new() do %Fib{} end def new(count) do %Fib{count: count} end end
次に、Enumerableを実装するが、以下の3つの関数の
実装が求められている。
def count(collection) :: {:ok, non_neg_integer} | {:error, module} def member?(collection, value) :: {:ok, boolean} | {:error, module} def reduce(collection, acc, fun) :: {:done, term} | {:halted, term} | {:suspended, term, continuation}
このうち、count/1, member?/2はデフォルトの実装が提供されていて、デフォ
ルトの実装を使う場合、{:error, module}を返すとよい。デフォルトの実装は、
reduce/3を使い、線形時間がかかるので、データ構造上、それよりも高速に求
められることがわかっている場合にのみ、カスタム実装を行うべき。
例えば、フィボナッチ数列のcountは、:infinityでない場合なら、
構造体のフィールドから直接求めることが可能。:infinityの
場合はデフォルト実装にお任せするようにする。
def count(%Fib{count: :infinity}), do: {:error, __MODULE__} def count(fib), do: {:ok, fib.count}
一方、member?/2は、結局1つひとつ比較するしかないので、デフォルト実装にお任せ。
def member?(_fib, _value), do: {:error, __MODULE__}
残りはreduce/3だが、アキュムレータにタグが付けられていることに注意して実装する。
- :cont - 数え上げを継続します。 {:cont, acc}か{:done, acc}を返します。
- :halt - 直ちに数え上げを停止します。 {:halted, acc}を返します。
- :suspend - 直ちに数え上げをサスペンドします。 {:suspended, acc, fn(x) -> reduce(enum, x, fun) end}を返します。
def reduce(e, acc, fun) do reduce(e.f0, e.f1, e.count, acc, fun) end def reduce(_f0, _f1, 0, {:cont, acc}, _fun) do {:done, acc} end def reduce(f0, f1, n, {:cont, acc}, fun) do reduce(f1, f0 + f1, n-1, fun.(f0, acc), fun) end def reduce(_, _, _, {:halt, acc}, _fun) do {:halted, acc} end def reduce(f0, f1, n, {:suspend, acc}, fun) do {:suspended, acc, &reduce(f0, f1, n, &1, fun)} end
このように、reduceではフィボナッチ数を計算しながらカウントが0になったら`{:done, acc}`を返すことになる。
そしてナイスなことに、この状態でStreamにも対応出来ている。
さてさっそく、使ってみる。お題として、10から100までのフィボナッチ数列の、最初の二つを出力するというものにしてみる。
iex(1)> import_file ("codes/protocol_fib.exs") {:module, Enumerable.Fib, <<70, 79, 82, 49, 0, 0, 14, 120, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 173, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 2, 104, 2, ...>>, {:__impl__, 1}} iex(2)> Fib.new |> Stream.filter(&Enum.member?(10..100, &1)) |> ...(2)> Stream.take(2) |> Enum.to_list [13, 21] iex(3)>
Fib.newとして、無限数列としているにも拘わらず、きちんと計算されて
いるのは、Streamのお陰。Enumerableプロトコルを実装するだけで遅延評価の恩恵を受けられるのは素晴しい。
elixir地域化の話: アプリケーションのロード
地域化の話ではあるけれど、Elixir/Erlangのコードローディングとアプリケーションの話でもある。
地域化の基本設計の段階で、翻訳リソースはアプリケーション毎(OTPでいうところのアプリケーション)に決定した。
ところが、Code.get_docs/2では、モジュールを指定してドキュメントを取得するので、モジュールからアプリケーションを検索する必要が出てきた。
appはどこ?
:application.get_application/1を使うと、モジュールからアプリケーションを検索してくれる。
iex(6)> :application.get_application(List) {:ok, :elixir} iex(7)>
しかし、現在ロードされていないアプリケーションに属するモジュールは答えてくれない。
iex(3)> :application.get_application(L10nIex) :undefined iex(4)> :application.load(:l10n_iex) :ok iex(5)> :application.get_application(L10nIex) {:ok, :l10n_iex}
アプリケーションをロードすると答えてくれるが、アプリケーションをロードするためにはアプリケーション名が分かっている事が必要。アプリケーション名が分からないから苦労しているというのになんとういうこと。
さてどうしたものか。
:code.is_loaded/1
まず、コードがロードされているなら、:code.is_loaded/1でそのファイルが分かるので、Code.ensure_loaded/1してからbeamファイルのパス名を取得する。
iex(27)> Code.ensure_loaded(L10nIex) {:module, L10nIex} iex(28)> :code.is_loaded(L10nIex) {:file, '/**(snip)**/l10n_iex/ebin/Elixir.L10nIex.beam'}
パス名が取得されたら、通常は同じディレクトリに*.appファイルがある筈なので、それを探す。pathをbeamファイルのパス名とすると、
app = Path.dirname(path) |> Path.join("*.app") |> Path.wildcard |> Path.basename(".app") |> String.to_atom
こんなふうにしてappファイルを取得し、その拡張子を除き、アトムに変換すると、OTPの規約ではアプリケーション名となるはず。毎回これを実施するのは大変なので、:application.load(app)しておく。
関係ないがパイプ演算子は只の第一引数を使い回すための糖衣構文なので、こんな事も当然出来る。
なお、さらりと書いているが、実際はerlangのドキュメントを調べまくってようやく分かったという。
成果物
これによって、ElixirでデフォルトでロードされるアプリケーションではないExUnit(ExUnitは同梱されるが、Elixir起動時に標準でロードされる訳ではない)に対しても、iexのExgettext.Helper.h/1コマンドや、ex_docのmake docsから翻訳リソースを参照することが出来るようになった。
これを使って、日本語のElixirリファレンスをアップデート。v1.0.1に対応。
elixir地域化の話その2
前回のExgettextは、ビルドが面倒だったり、mixのdepsへ入れると
うまく連動しないとか不満があったので、その辺を整備してみました。
不満1 mix depsに入れるとexmoがビルドされない
当たり前で、deps.compileではMix.Tasks.Compileが呼ばれます。Mix.Tasks.l10n.msgfmtなどという意味不明なタスクは呼ばれません。depsの設定でカスタムコンパイラを設定する事もできますが、それはmakeなどを想定しているもので、うまく連動しません。
そこで、Mix.Tasks.Compile.Poという新しいコンパイラタスクを作成しました。やっている事は、Mix.Task.runでl10n.msgfmtを呼び出すだけです。
mixはコンパイラをMix.compilersという設定で持っていて、Mix.Project.configで上書きできます。したがって、mix.exsに
defmodule L10nElixir.Mixfile do use Mix.Project def project do [app: :l10n_elixir, version: "0.0.1", elixir: "~> 0.15.1-dev", compilers: Mix.compilers ++ [:po], deps: deps] end def deps, do: [] end
のようにしておけばよいです。
不満点2 Code.get_docs/2が面倒くさい
Code.get_docs/2は、モジュールからドキュメントを抽出する関数ですが、この時点で翻訳リソースを出力してほしいのですが、Codeモジュールに手を入れるのはチキンな私には出来ません。そこで、aliasを使い、ごまかしを企むことにしました。
use Exgettext ... Code.get_docs(Code, :all)
とすると、CodeモジュールはExgettext.Codeモジュールを参照するという仕組みです。そのためには、Exgettext.Codeモジュールは、Codeモジュールの全ての公開関数を実装している必要があります。イメージは、
defmodule Exgettext.Code do Elixir.Code.__info__(:exports) |> Enum.filter(&(&1 !== {:get_docs, 2})) |> Enum.map(&(defdelegate &1, to: Elixir.Code)) def get_docs do something end end
という感じで、get_docs/2以外をぜーんぶdelegateしてしまおうという
企みです(上のコードは動作しません)。
これを、defdelegate_filter/3という形で実装しました。
使い方は、
defmodule Exgettext.Code do defdelegate_filter(Exgettext.Code, Code, &(not &1 in [{:get_docs, 2}])) def get_docs(module, kind) do ... end end
という感じです(実際はモジュール修飾がついてExgettext.Util.defdelegate_filterとかかっこ悪くなります)。
defdelegate_filterは、大きく4つの仕事をします。
1. ターゲットの公開関数を取得
2. ユーザから渡されたフィルタの適用と、デフォルトで実装される関数(__info__, module_info)を除外
3. アリティから仮引数の作成
4. 関数名、仮引数のリストから、defdelegate funcname(arg1,..argn), to: target
を作成して、それを実行
defdelegate funcname(arg,..,argn), to: targetは、quoteすると分かりますが、
{{:., [], [Kernel, :defdelegate]}, [], [{funcname, [], args}, [{:to, target}]]}
です。argsは、{:aN, [], nil}のリストです(Nは整数)。このタプルを作るのが前半です。
後半の実行は、Code.eval_quoted/2ではなく、Module.eval_quoted/2を使います。
それらを盛り込むと、こんな感じに実装できます。
def defdelegate_filter(src, target, func) do target.module_info(:exports) |> Stream.filter(fn({ff, a}) -> func.({ff, a}) && (not ff in [:__info__, :module_info]) end) |> Stream.map(fn({ff, a}) -> args1 = :lists.seq(1,a) {ff, Enum.map(args1, fn(x) -> {:"a#{x}", [], nil} end)} end) |> Enum.map(fn({ff, a}) -> # IO.inspect ff r = {{:., [], [Kernel, :defdelegate]}, [], [{ff, [], a}, [{:to, target}]]} # IO.puts Macro.to_string(r) Module.eval_quoted(src,r) end) end
ex_docへのパッチ
パッチはExgettext.Codeを使うためのalias一行ですみます。
diff --git a/lib/ex_doc/retriever.ex b/lib/ex_doc/retriever.ex index 40b07d9..7f3ecde 100644 --- a/lib/ex_doc/retriever.ex +++ b/lib/ex_doc/retriever.ex @@ -1,3 +1,4 @@ +use Exgettext defmodule ExDoc.ModuleNode do defstruct id: nil, module: nil, moduledoc: nil, docs: [], typespecs: [], source: nil, type: nil air13:ex_doc k-1$
実際は、ロードパスの追加等他の要素もありますが。。。
システムへのインストール
この日本語環境をデフォルトで使用したい場合、exgettext, l10n_iex, l10n_elixirのそれぞれのディレクトリでmix archive.buildを実行して、ezパッケージを作成し、/usr/local/lib/elixir/libなどへコピーしてからunzipで展開すると、デフォルトで使えるようになります。
~/.iex.exsに、以下を記述するとhコマンドで翻訳済みのドキュメントを参照できるので幸せになります。
import Exgettext.Helper
成果物は、GitHub - k1complete/exgettext: yet another gettext localization package for elixir、
GitHub - k1complete/l10n_iex: localization of IEx、
GitHub - k1complete/l10n_elixir: l10n for elixir、
GitHub - k1complete/ex_doc: ExDoc produces HTML and online documentation for Elixir projectsあたりにおいてあります。
[elixir] Elixir地域化の話
依然として開発が活発なElixirですが、日本語のサポートが欲しいと思って、地域化のパッケージを作成してみました。国際化/地域化パッケージと言えばGNU gettextですので、できるだけGNU gettextを使うようにしました。
Exgettext
地域化するためにはまず、
- メッセージの拾い出し(xgettext)
- メッセージのデータベース化(msginit)
- メッセージのメンテナンスフレームワーク(msgmerge)
- アプリケーション実行時のサポート(msgfmt, gettext)
が必要です。これらをelixirらしくl10n.というプレフィックスをつけたmixタスクにしています。
メッセージの拾い出し
これはsigil Tを使ってマークアップしてもらうことにしました。
sigil Tではソースコンパイル時にデータベースへ格納します。
また、@doc, @moduledocも収集します。こちらのほうはmix l10n.xgettextユーティリティ実行時にモジュールからCode.get_docs/2を使って収集します。これでpotファイルを作成します。
メッセージのデータベース化
mix l10n.msginitで初期メッセージデータベースを作成します。現在のLANG環境変数から言語を判断します。これは翻訳言語当たり一度だけ行います。これでpotファイルからpoファイルが生成されます。
メッセージのメンテナンスフレームワーク
poファイルは只のテキストファイルですのでエディタで編集できますが、emacsのpo-modeを使うのが楽です。何れにしてもGNU gettext互換ですのでいろいろなツールが使えます。
もとのプログラムがバージョンアップした場合には、あたらしいpotファイルに対してl10n.msgmergeを使う事でマージされたpoxファイルが生成されます。これを確認してpoファイルとして使用することができます。このあたりのワークフローもGNU gettextと同様の習慣に従っています。
実行時のサポート
翻訳が終了したら、l10n.msgfmtでメッセージオブジェクトを生成します。このフォーマットは残念ですがGNU gettextとは関係ありません。
実態はdetsデータベースになっています。poファイルやpotファイルと同様、このデータベースもpriv/配下に配置されます。というのはmix archiveでバイナリ配布パッケージを作成する際に同梱されるからです。
一方sigil Tマクロは、コンパイル時に「このデータベースを参照してメッセージをコンバートする」というgettext関数呼び出しに展開されています。
これにより、実行時にLANG環境変数に応じたメッセージが使用されます。
翻訳の単位
GNU gettextにならって、アプリケーション単位にpoファイルを作成するようにしています。Elixir的にはmixファイルのproject.apps単位となります。sigil Tはモジュール単位にimportする必要があります。
サンプルプログラム
せっかくなのでIExのヘルプをExgettextを用いて翻訳しています。
bash-3.2$ mix do deps.get, deps.compile, compile * Getting exgettext (https://github.com/k1complete/exgettext.git) Cloning into '/../l10n_iex/deps/exgettext'... (...) Generated l10n_iex.app bash-3.2$ mix l10n.msgfmt msgfmt for l10n_iex priv/po/ja.po /(...)/l10n_iex/_build/dev/lib/l10n_iex/priv/lang/ja/l10n_iex.exmo bash-3.2$ iex -S mix Interactive Elixir (0.14.0-dev) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> h(c/2) * def c(files, path \\ ".") Expects a list of files to compile and a path to write their object code to. It returns the name of the compiled modules. When compiling one file, there is no need to wrap it in a list. ## Examples c ["foo.ex", "bar.ex"], "ebin" #=> [Foo,Bar] c "baz.ex" #=> [Baz] iex(2)> import Exgettext.Helper nil iex(3)> h(c/2) * def c(files, path \\ ".") コンパイルするファイルのリストとオブジェクトコードを 書き出すパスを指定します。コンパイルされたモジュールの名前を 返します。 一つのファイルをコンパイルする時にリストにする必要はありません ## 例 c ["foo.ex", "bar.ex"], "ebin" #=> [Foo,Bar] c "baz.ex" #=> [Baz] iex(4)>
これらは
GitHub - k1complete/l10n_iex: localization of IEx、GitHub - k1complete/exgettext: yet another gettext localization package for elixirに公開しています。まだexgettextは荒削りですので微妙な点があるかもしれません。