昨夜、Dave Thomas氏の「Programming Elixir」を読み終えて、こうしてブログを書いている。
読み終えて「とんでもないところに足を踏み入れたな」と思う。漂うのは、何もかもが今までのプログラミングモデルとは違うという圧倒的なアウェイ感である。
Dave Thomas氏は度々このブログにも登場しているが、『達人プログラマー』の著者であり、アジャイルソフトウェア開発宣言の発起人の一人でもある。近年はRubyの人という感じであったが、ElixirにRuby以来の衝撃を受けて、この「Programming Elixir」を執筆するに至ったとのこと。
この本が素晴らしいのは、元々はオブジェクト指向のプログラマーであった著者が関数型言語であるElixirに出会って、徐々にそのコンセプトに体を慣らせて行く過程を、読者が追体験できるところである。この本で扱っているのは Elixir やその基礎となっている Erlang/OTP のほんの導入部分のみであり、主眼はオブジェクト指向プログラマーが関数型あるいは並行指向プログラミングへとパラダイムシフトするための手助けをするといった趣向になっている。
「Programming Elixir」のサンプルプログラムを動かしながら一通りのトピックを見て回り、Elixir(というかErlang)で最も重要なのは、関数型プログラミングではなく、「並行指向プログラミング」と呼ばれる、そのプロセス管理の仕組みであることが分かった。関数型言語はあらゆるプラットフォームで利用出来るが、この並行処理のモデルはおそらくErlangにしかないものなのだろう。
Elixirの並行処理の威力を見るにはサンプルコードを実行するのが手っ取り早い。
まず、Elixirのサイトにある一番始めのサンプルコード(Hello world)が、いきなり並行処理の例になっている。
parent = self() spawn_link(fn -> send parent, {:msg, "hello world"} end) receive do {:msg, contents} -> IO.puts contents end
これを hello.exs
というファイルに保存して、
$ elixir hello.exs
のように実行すると、
hello world
となる。
Elixirを知らない人でも、このコードが何をしているかは、何となく分かるのではないだろうか。
spawn_link
という関数に渡された関数が、このプログラムを実行しているメインのプロセスとは別のプロセスとして起動されて、関数の内容を実行する。- 新しく作られたプロセス側では、メインプロセス側(
parent
)に “hello world” というメッセージを送る。 - メインプロセス側は、どこからかメッセージが来ないかを待ち受けて(
receive
)、メッセージが来たらそれをコンソールに表示する。
このように、spawn
でプロセスを作り、send
と receive
でメッセージのやり取りをするという単純な仕組みである。プロセスの間でのやり取りはこのメッセージ交換だけで、可変データを共有するということがないので、従来の並行処理について回る排他処理などが必要にならないというのが Elixirの強みである。
この例では一つのプロセスを作っただけなので、他の言語でも同様のことは簡単に出来るかもしれない。では、以下の例だとどうだろうか。
defmodule Chain do def counter(next_pid) do receive do n -> send next_pid, n + 1 end end def create_processes(n) do last = Enum.reduce 1..n, self, fn (_, send_to) -> spawn(Chain, :counter, [send_to]) end send last, 0 receive do final_answer when is_integer(final_answer) -> "Result is #{inspect(final_answer)}" end end def run(n) do IO.puts inspect :timer.tc(Chain, :create_processes, [n]) end end
いきなり複雑な例になったが、やっていることは単純である。run
関数が呼ばれると、指定された数(n
)だけのプロセスを次から次に立ち上げて、それらのプロセスには順番に次のプロセスへの参照を持たせて数珠つなぎのような構造にする(終端はメインプロセス)。全てのプロセスを立ち上げたら、先頭に数値「0」を送ると、プロセスの間でバケツリレーをしながら、その数値に「1」を加えて行く。最後はメインプロセスに到達して結果の値をコンソールに書き出す。この一連の処理は、Erlangの関数である :timer.tc
によって時間が計測されており、結果とともに経過時間も出力される。
さて、このコードを chain.exs
というファイルに保存して、以下のように実行したらどうなるだろうか?
$ elixir --erl "+P 1000000" -r chain.exs -e "Chain.run(1_000_000)"
生成するプロセスの数はなんと100万個である。デフォルトのセッティングでは同時並行プロセス数の上限に達してしまうので、オプション(--erl "+P 1000000"
)で上限を変更している。
これを筆者のPC(Macbook Pro – 3 GHz Intel Core i7, 16GB RAM)で実行した結果が以下である:
{7011658, "Result is 1000000"}
一つ目の数字の単位はマイクロ秒なので、実に100万個のプロセスを起動して連携させる処理がたったの7秒程度で終わったということになる。
何故このようなことが可能なのかと言えば、これらのプロセスがOSで管理されているプロセスやスレッドなどではなく、Erlang VMの中で管理されている独自のプロセスだからである。このErlangが提供する極めて軽量なプロセスモデルが、Elixir/Erlang環境が単なるプログラミング言語以上のインパクトを持つ所以となっている。
このようにElixirでは、一つのプロセスが極めて軽量なため、一つのプロセスを作るのは、オブジェクト指向システムで一つのオブジェクトを作るのと同じぐらいの感覚であると「Programming Elixir」では説明されている。実際プロセスの中には状態を持つものもあり、それらがメッセージでやり取りをするとなれば、モデル的にはオブジェクトと変わりないように思える。Elixirでは、状態を持つプロセスを「Agent(エージェント)」として容易に管理する仕組みがあり、これは、かつてポスト・オブジェクト指向の有力候補と呼ばれていたエージェント指向ではないかと、はたと思い当たったが、それは多分違うような気がする。
- エージェント指向が目指すもの [連載第1回 サル思考とエージェント指向]
- エージェントは生き残っているか?(前編)