IT + X

ITと何かのコラボレーションは時代を変えるかも

Elixirを使ってみたらとても楽しかった

Elixirはマルチプロセスを前提とした関数型プログラミング言語でエリクソンが電話交換機プログラムのために開発したErlangという仮想マシンシステム上で動作します。
Elixirが流行ってきた背景として、CPUが複数コアで構成されるようになり個人のパソコンでも8コアや16コアのCPUが使われるようになってきたのがあると思います。
しかし、CやJava,Pythonなどの現在主流の言語はシングルコアのCPUを前提にした言語なのでマルチコアをうまく扱えません。
そこでGoやRustなど新しい言語が登場してきたのですが、Elixirはその中でも比較的とっつきやすく関数型プログラミングのよさもうまく取り込んだ言語です。
ただ、古いタイプの言語に慣れている人にはそこがとっつきにくい部分かもしれません。

最新の情報を得たかったのでこちらの本で学びました。

Programming Elixir ? 1.6: Functional |> Concurrent |> Pragmatic |> Fun

Programming Elixir ? 1.6: Functional |> Concurrent |> Pragmatic |> Fun

Elixirを使ってみて私がいいなと思ったのはこんなところでした。

  • Lispっぽいリスト
  • mapや再帰を使ってループを使わない
  • 関数シグネチャーを使って条件分岐を行う
  • 関数を複数つなぐパイプオペレーター
  • 複数プロセス間のメッセージ通信が簡単

Lispっぽいリスト

昔、EmacsというエディタでLispという言語を使ったことがありますが、Elixirの配列はLispによく似てると思いました。
例えば、Lispはリスト構造が基本でcarやcdrというリストの先頭やそれ以外を取得する関数があります。
それと同じような機能がElixirにもあります。
例えば、配列の先頭を取得するには以下のように書きます。

iex > [ head | tail ] = [ 1 , 2 ,3 ,4 ,5 ]
iex > head
1
iex> tail
[ 2, 3, 4, 5 ]

ちなみにiexはElixirのシェルみたいなものでRubyでいうirbと同じものです。 この言語を作った人は明らかにLisperですね。

また、mapというデータタイプを使うことができます。

iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1

mapとはキーと値がペアになっているデータで、他の言語では連想配列と呼ばれているものです。
ちなみに配列の添字に使われている:a や:bはアトムと呼ばれている定数データで見た感じ文字列のように見えますが スキャン処理などを高速に行えるようにハッシュ値のようなデータです。
Rubyのシンボルを同じですね。

再帰を使ってループを使わない

Lispでは再帰呼び出しというテクニックをよく使いますが、Elixirでもよく使われます。 再帰とは関数の中で自分自身を呼ぶことをいいます。
例えば、フィボナッチ数を計算する関数はこんな感じです。

def fib_calc(n) do
   result = fib_calc(n - 1) + fib_calc(n - 2)
end

フィボナッチ数は最初0と1から始まるので停止条件として書いておくべきなのですが上の例では省略しました。
CやJavaで書く場合はforループなどで集計していくことが多いですが、再帰で書くことによってシンプルな記述になるところがいいですね。
ただ、再帰は慣れていないと訳がわからないコードになりがちなのでプログラマーの習熟が必要です。
また、再帰は普通スタック領域をたくさん消費したり実行速度が遅かったりするのですが、Elixirではうまく最適化してくれているようです。

関数シグネチャーを使って条件分岐を行う

例えば、C言語でエラー処理のコードはこんな感じになります。

func1(int val , int error_code){
   if (error == 0){
          do_something(val);
   }else{
         do_error(error_code);
   }
}

Cでコード書いていると条件分岐でエラー処理だらけになることが多いです。
一方、Elixirではこういう書き方ができます。

defmodule m1 do
    def func1( {:ok, val}) do
           do_something(val)
    end
    def func1({:error, error_code}) do
            do_error(error_code)
     end
end

波括弧の引数はタプルと呼ばれているデータ型で変更できない配列です。
上の例では同じ名前で引数の違う関数func1が定義されます。
Elixirでは同じ関数名でも引数を変えることにより別の関数として定義することができます。
したがって、この例では:okを引数に与えると通常の処理をして:errorを与えるとエラー処理が行われます。
このように、Elixirではifなどの条件分岐をできるだけ使わないようにしてコードの見通しをよくすることができます。

関数を複数つなぐパイプオペレーター

シェルをご存知の方はコマンド同士をつなぐパイプという機能があるのをご存知だと思います。

# cat abc.txt|grep "ABC" | wc -l

Unixのコマンドは一つ一つは単純な処理しかできませんが、パイプで組み合わせることによって様々な処理を行わせることができます。
Elixirにもパイプオペレーターというものがあり、関数をつないで複雑な処理を行わせることができます。

iex> "Elixir rocks" |> String.upcase() |> String.split()
["ELIXIR", "ROCKS"]

上の例では|>がパイプオペレーターで"Elixir rocks"という文字列がString.upcase()関数の第一引数として与えられます。
つまり、String.upcase("Elixir rocks")を呼んでるのと同じことになります。
次のString.split()も同様にデータが流れます。
Unixエンジニアにはあたかもシェルスクリプトを書いているみたいでたまらない機能ですね。

大量プロセスを起動してもオーバーヘッドが少ない

Elixirでは簡単に複数のプロセスを立ち上げて並列で処理させることができます。
ここでいうプロセスとはOSネイティブのプロセスではなくElixirのVM上で実行するプロセスなのでとても軽いものになっています。
プロセスの生成は以下のように行います。

spawn(module_name, function_name, [function_params])

例えば、複数のプロセスで合計するプログラムはこんな感じになります。

defmodule ParalellSum do
    def psum(n) do
        result = Enum.reduce(n, fn(x, acc) -> x + acc end)
        result
    end
    def do_process(n) do
        num_processes = n
        calc_val = [1,2,3,4,5]
        Enum.each 1..num_processes, fn(_) -> spawn(ParalellSum, :psum, [calc_val]) end
    end
    def run(n) do
        :timer.tc(ParalellSum, :do_process, [n])
        |> IO.inspect
    end
end

大まかに説明すると、spawn(..)でプロセスを生成してpsum()という関数を別プロセスで並列に動かしています。
psum()関数はcalc_valの要素を合計します。
このプログラムをプロセス数1000,10000,100000で動かした結果がこちらです。

$ elixir -r myprocess.ex -e "ParalellSum.run(1000)"
{1756, :ok}
$ elixir -r myprocess.ex -e "ParalellSum.run(10000)"
{18173, :ok}
$ elixir -r myprocess.ex -e "ParalellSum.run(100000)"
{156408, :ok}

run関数のパラメータがプロセス数で結果の数値は実行時間をマイクロ秒で出力します。
ご覧の通り、10万プロセスで0.1秒ほどしかかかっていません。
ちなみに実行環境はMacBookPro Corei5 2.3GHzでした。
プロセスによるオーバーヘッドはほぼないと考えて開発できますね。

複数プロセス間のメッセージ通信が簡単

Elixirの一番の売りはプロセス間通信が簡単なことです。
次の例はエコーサーバーを生成してクライアントのメッセージをそのまま返すプログラムです。

defmodule Echo1 do
    def echo_back do
        receive do
            {sender, msg} ->
                send sender, {:ok, "#{msg}" }
        end
    end
end

# client
pid = spawn(Echo1, :echo_back, [])
send pid, {self(), "Hello World !"}

receive do
    {:ok, message} ->
        IO.puts message
end

spawn()関数でエコーサーバーを生成してクライアントから"Hello World!"というメッセージを送信するとサーバーがそのまま返します。
send pid,...がサーバーにメッセージを送信しているところで、echo_back関数のreceive do...のところで受信しています。
{send, msg}->のところは送られてきたメッセージのパターンを記述するところでマッチすればそれ以下の処理が実行されます。
したがって、このパターンを複数持たせればメッセージパターンによって違う処理をさせることができます。 LinuxC言語でデーモンを書くよりはるかに簡単ですね。

ざっと触ったところではこんな感じでした。
また、ElixirにはRailsに似たウェブフレームワークPhenixや組み込み系フレームワークのNervesなどがあっておもしろそうです。
Elixirはマルチプロセスで通信しながら処理を行うのが当たり前なのでRubyPythonとは違った作りにできると思います。
次回ウェブアプリ開発を行うときはElixirを使ってみたいなと思いました。