テストステ論

高テス協会会長が, テストステロンに関する情報をお届けします.

(elixir-report) Elixirの内部表現を実例で調べてみる

Elixirとは?

私は, 私のいる骨業界の中ではかなり言語好きな方であると思う. そんな私が今もっともアツく感じているのはElixir. RakutenTech 2013でもプレゼンがあったようで, 日本のウェブ業界ではすでに注目を集めている言語のようである.

[RakutenTechConf2013][B-1] Elixir: The Joy of Ruby, the Power of Erlang - YouTube

Elixirの特徴はざっくりいうと:

私がもっとも注目しているのは, Elixirのメタプログラミング言語としての側面である. そこで実験として, ElixirでRakeのクローンを作ってみるというプロジェクトを始めた. ElixirにはすでにMixがあるが, Rakeのように使い勝手が良いとは思わない. Rakeの完全なクローンを作ることは無駄ではないと思うし, もし要求があればMixの方に「Rakeの文法を提供するモード」としてマージすることも可能性としてはある. しかしとりあえずは「私が欲しいから」「やってみると勉強になるから」という理由で始めてみることにした.

初歩的な解析

ひと通りネットにあるブログなりを読むと, そこからは, あとはトライアンドエラーをしてがんばってねーの世界に放り出される. 発展途上の言語はいつだってそう. Rustの時もそうだった. しかし, ElixirはRustよりはツール周りがいくらかまともなようで, REPLは本当に使い勝手が良い.

今回はまず, defがどう表現されているかを見てみる. (この目がくらみそうなかっこ, どこかで見覚えがありませんか)

ex(1)> quote do: (defmodule M do def f (x) do x + 1 end end)
{:defmodule, [context: Elixir, import: Kernel],
 [{:__aliases__, [alias: false], [:M]},
  [do: {:def, [context: Elixir, import: Kernel],
    [{:f, [context: Elixir], [{:x, [], Elixir}]},
     [do: {:+, [context: Elixir, import: Kernel], [{:x, [], Elixir}, 1]}]]}]]}

内部表現を見るには, quote do: xxxを使う. これは, quote do xxx endと同じであり(do endの方が糖衣構文), xxxのElixirコードに対応する内部表現を返す. さらに, do: xxxの部分はkeyword listであり, 本当はquote([do: xxx])と書くところを, ()と[]を省略した結果である. だから, 以下のように書いても同じ内部表現が得られる.

ex(8)> quote([do: (defmodule M do def f (x) do x + 1 end end)])
{:defmodule, [context: Elixir, import: Kernel],
 [{:__aliases__, [alias: false], [:M]},
  [do: {:def, [context: Elixir, import: Kernel],
    [{:f, [context: Elixir], [{:x, [], Elixir}]},
     [do: {:+, [context: Elixir, import: Kernel], [{:x, [], Elixir}, 1]}]]}]]}

Elixirの内部表現は, リテラルを除き, {A, B, C}のトリプルの形をしている. これについては日本語のブログで解説がある.

例えば, ifは, 以下のように表現される. ざっくりいうと, Bはあまり気にする必要がある. 大事なのはAとCである. Aには「それが何なのか」が入っており, Cには「そのために何が必要なのか」が入っていると考えればいい.

iex(9)> quote do
...(9)>   if true do end
...(9)> end
{:if, [context: Elixir, import: Kernel], [true, [do: nil]]}

本題に戻してdefの内部表現を拾うと, 以下のようになっている.

{:def, [context: Elixir, import: Kernel],
    [{:f, [context: Elixir], [{:x, [], Elixir}]},
     [do: {:+, [context: Elixir, import: Kernel], [{:x, [], Elixir}, 1]}]]}

上のトリプルに照らし合わせると,

  • A = :def
  • B = [context: Elixir, import: Kernel]
  • C = [{:f, [context: Elixir], [{:x, , Elixir}]}, [do: {:+, [context: Elixir, import: Kernel], [{:x, , Elixir}, 1]}]]

Cは要素2つのリストである.

  1. f (x)の部分. fという関数が, xという引数をとることが書かれている.
  2. xと1の足し算が行われることが書かれている.

仮に以下のように複雑にするとどうなるか?

iex(14)> quote do: (defmodule M do def f x, y do z = x + 1; z + y end end)
{:defmodule, [context: Elixir, import: Kernel],
 [{:__aliases__, [alias: false], [:M]},
  [do: {:def, [context: Elixir, import: Kernel],
    [{:f, [context: Elixir], [{:x, [], Elixir}, {:y, [], Elixir}]},
     [do: {:__block__, [],
       [{:=, [],
         [{:z, [], Elixir},
          {:+, [context: Elixir, import: Kernel], [{:x, [], Elixir}, 1]}]},
        {:+, [context: Elixir, import: Kernel],
         [{:z, [], Elixir}, {:y, [], Elixir}]}]}]]}]]}

関数の定義は, トリプルのCに[{:x }, {:y}]が列挙されている.

     [do: {:__block__, [],
       [{:=, [],
         [{:z, [], Elixir},
          {:+, [context: Elixir, import: Kernel], [{:x, [], Elixir}, 1]}]},
        {:+, [context: Elixir, import: Kernel],
         [{:z, [], Elixir}, {:y, [], Elixir}]}]}]]}]]}

doの方は, (2行に渡っているという理由からか)__block__というノードが一つ追加されて, 1つ目のz = x + 1と, 2行目のz + yがそれぞれ__block__のCとして列挙されている. この事から, do内の表現は, その複雑さによって変わってくるということが分かる. doの中身に対してマッチングするということは危険であると考える.

ところで, 今回解析したdefは, マクロとして実装されている. Elixirは, 言語のコアは小さく保ち, マクロの力で他の部分を構築するというかなり挑戦的な言語である. 何がマクロで実装されているかはhttp://elixir-lang.org/docs/stable/elixir/Kernel.htmlに書いてある. 他には, ifもマクロである(unlessがマクロだと言われても驚かないと思うが, ifまでもがマクロである).

今回は, Elixirの内部表現について, defを例題として解説した. Rakeクローンを作るのは, なかなか骨である. 今回はそのフェイズ1として, 基本的なことをきっちりと解析することに注力した.