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に置いてみた。

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

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の受け売りです

[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さんです。

*1:当時のマッチ構文はmatch: xxであったが、ここは今風にアレンジしてある

*2:複数の引数を使いたい場合には、タプルなどでごまかしてもらう事にする。

*3:名前の種類が増えると空間が汚れるので

*4:原始再帰と合成で構築できる関数

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

地域化するためにはまず、

  1. メッセージの拾い出し(xgettext)
  2. メッセージのデータベース化(msginit)
  3. メッセージのメンテナンスフレームワーク(msgmerge)
  4. アプリケーション実行時のサポート(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 IExGitHub - k1complete/exgettext: yet another gettext localization package for elixirに公開しています。まだexgettextは荒削りですので微妙な点があるかもしれません。