このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

僕もErlangを試してみたよ -- 軽量プロセスを中心に

えっ、Erlang(アーラン)を紹介するのが流行なの? フーン、じゃ僕もやってみよう。

というわけで、ちょっと試してみたので、感想+解説を書きます。あまり他で触れられてないようなネタを選ぶつもり。

内容:

  1. 予備知識への参照
  2. 構文はPrologじゃん
  3. ランタイム・システムとEシェル
  4. で、Erlangって何がいいのよ?
  5. 繰り返しとプロセス
  6. メッセージング=プロセス間通信
  7. 最後の例題:greetingプロセス
  8. もう一度、Erlangって何がいいのか?

●予備知識への参照

HelloWorldを書くまでの案内は:

Erlangの特徴は、次のスライドによくまとまってます。

いちおう本家本元も。

●構文はPrologじゃん

パッと見、構文がProlog(プロローグ)ソックリ。えっ、最近の若いもんはProlog知らないの? “第五世代プロジェクト”の世代(=ジッサマ)としてはけっこうなつかしいのう、Prolog。この構文なら、なんとかなりそうだわさ。

参考までに、以下はPrologの典型的なプログラム。2つのリストをアペンド(連接、つなぎ合わせ)します*1


% 第3引数が、第1引数と第2引数をアペンドした結果
append([], List, List).
append([Head|Rest1], List, [Head|Rest2]) :-
append(Rest1, List, Rest2).

第3引数を戻り値だと思い直して、記号「:-」を「->」に置き換えればErlangプログラムになるんじゃないの、たぶん:


append([], List) -> List.
append([Head|Rest1], List) ->
[Head|append(Rest1, List)].
あんれぇ、「同名の関数が2回定義されている」というエラー。ウーン? 関数定義をピリオドで分けて書けないらしい。… ピリオドをセミコロンにすればOKだった。結局:

% ファイル:apppend.erl

% 以下、Erlangのお約束
-module(append).
-export([append/2]).

% 戻り値が、第1引数と第2引数をアペンドした結果
append([], List) -> List; % ここはセミコロン!
append([Head|Rest1], List) ->
[Head|append(Rest1, List)].

コマンドerlでErlangランタイム・システムを立ち上げて実行してみましょ。


Eshell V5.5.4 (abort with ^G)
1> c(append).
{ok,append}
2> append:append([], []).
[]
3> append:append([], [1]).
[1]
4> append:append([1, 2], [3, 4]).
[1,2,3,4]
5>

なんかうまくいってる*2。構文はこれでわかったつもり(←オイ、コラ!)。

●ランタイム・システムとEシェル

OSからのコマンドerl(または、ウィンドウを開くwerl)で、Erlangランタイム・システム(ERTS;以下、ランタイムと略称)が起動します。これは、Erlangの仮想機械(VM)と仮想OS(実行時管理機構)が一緒になったようなもの。ランタイム=実行環境に入ってしまえば、そこはホストOSとは独立したErlang独自の世界となります。

erlの実行により表に登場するEシェル(Erlang shell)は、この仮想コンピュータのコンソールのようなもの。注意すべきは、Eシェルは完全なErlangインタープリタではないってこと。Eシェルはランタイムへの対話的窓口を提供しますが、Erlangの任意の宣言/式を評価できるわけじゃないよ。例えば、Eシェルから関数定義はできません(ファイルに書く必要がある)。Eシェル特有のコマンドはhelp().(ピリオドは、式やコマンドの終端を示す)で一覧できます。例えば、c(名前).は、名前で指定されたモジュール(とりあえずファイルだと思ってよい)をコンパイルします。

cコマンドがあることからわかるように、Erlangは「仮想機械+コンパイラ」方式(このような分類は、さほどの意味はないけどね)。コンパイル結果であるバイナリは、.beamという拡張子になるようです。たとえば、<Erlangをインストールしたディレクトリ>/lib/ を眺めてみると、<名前>-<バージョン番号>/ なるサブディレクトリの下にモジュールのグループ(正式になんと呼ぶのか知らない*3 )の実体が格納されています。実行可能形式は ebin/ というサブディレクトリに置くのがお約束らしく、.beamファイル群が <名前>-<バージョン番号>/ebin/ に集めてありますね。

●で、Erlangって何がいいのよ?

さて、ここで僕の感想を挿入。

言語仕様だけを見ると、あんまりパッとしないな。関数型言語といっても、その純度は低いですね。副作用は普通に使うし、基本的制御構造は順次実行だし、ラムダ式も付け足しっぽい。僕はたまたま構文に違和感はなかったけど、奇妙な構文にアレルギーを起こす人もいそう。

準備されているデータ構造が低レベルで(例えば、まともな文字型も文字列型もない)、情報隠蔽やデータ抽象の手段が貧弱で辛い。動的型付けには賛否両論があるでしょうが、コンパイラ型推論してくれたほうがうれしいなー(静的型付けの試みはあるようです*4)。

だがしかしErlang、気に入った。いいね、これ!

まずね、洗練されてパーフェクトなものより、少し奇妙で不格好なほうが魅力的だってことがあるでしょ。そういう魅力があるね、Erlang。そして、ランタイムの能力が素晴らしい。軽量プロセスをふんだんに使ってもキチンと動作するランタイムはたのもしいっす。歴史と実績に裏付けられている、ってことだろね。

●繰り返しとプロセス

HelloWorldを出発点にして、軽量プロセスの話をしましょう。


-module(hello).
-export([greet/0]).
% ↑開発中は、-exportを書くより、
% -compile(export_all). ってしておくと便利だよ。

greet() -> io:fwrite("Hello, world.\n").

Eシェルのプロンプトには「12>」のように通し番号が付くのだけど、以下番号は省略しますね。


> c(hello).
{ok,hello}
> hello:greet().
Hello, world.
ok
>

繰り返しは次のように書きます。


greet_repeat(0) -> ok;
greet_repeat(N) when N > 0 ->
greet(),
greet_repeat(N-1).


> hello:greet_repeat(3).
Hello, world.
Hello, world.
Hello, world.
ok
>

最後に自分自身を呼び出す(末尾再帰といいます)方法で繰り返しを書くのね。この書き方だと最適化されて、スタックを消費することはありません。なので、次の関数は無限に走り続けます。


greet_forever() ->
greet(),
greet_forever().

Eシェルから、hello:greet_forever().を実行してしまうと、ランタイムを強制終了する以外に止める方法はありません*5。そこで、別プロセスでhello:greet_foreverを実行して、Eシェルのプロンプトからデタッチしましょう。ここでプロセスと言っているのは、もちろんOSのプロセスではなくて、Erlangランタイム内で走行する極めて軽量のプロセスのことです。

別プロセスであっても、出力がガンガン表示されると操作しにくいので、スローダウンしときますね。


greet_forever_slowly() ->
greet(),
timer:sleep(3*1000), % 少し休みましょう
greet_forever_slowly().

別プロセスを生成して、それにhello:greet_forever_slowlyを実行させるには:


> spawn(hello, greet_forever_slowly, []).
Hello, world.
<0.55.0>
Hello, world.
Hello, world.
Hello, world.
3秒の間隔があるので、終了コマンドq().は打てます :-)

●メッセージング=プロセス間通信

hello:greet_forever_slowlyでは3秒間sleepしてますが、この待ち時間にメッセージをチェックして、何かメッセージがあれば終了するプログラムにしましょう。ここで、メッセージとはErlangのプロセス間通信のメカニズムで、各プロセスに付いているメッセージキュー(メールボックス)をreceiveで読み出す方式です。


% 新しいgreet_forever_slowly
greet_forever_slowly() ->
greet(),
receive
_Any ->
io:fwrite("~w, exit.\n", [self()]),
exit(ok)
after 3*1000 -> ok % タイムアウト、何もしない
end,
greet_forever_slowly().


> spawn(hello, greet_forever_slowly, []).
Hello, world.
<0.51.0>
Hello, world.
Hello, world.
Hello, world.
Hello, world.

ここで、<0.51.0>はプロセスID(PID)です。プロセス生成関数spawnの戻り値なので、P = spawn(hello, greet_forever_slowly, []).のように変数に代入(束縛)しておくと、後でPIDを使うとき便利です。ただし、Erlangの変数は1回しか代入できないので注意してください! 必要なら、Eシェルのf()コマンドで変数束縛をクリアしてください。

さて、PIDを目安にして、目的のプロセスにメッセージを送れます。その構文は:

  • PID ! <メッセージのデータ>

または、

  • erlang:send(PID, <メッセージのデータ>)

メッセージのデータはなんでもよくて、Erlangの任意のデータを送れます。PIDのリテラル表現はない(<0.51.0>は単なる表示形式)ので、PIDが入った変数Pに対してP!stop.とするか、pid(0, 51, 0)!stop.のようにします。


> P = spawn(hello, greet_forever_slowly, []).
Hello, world.<0.59.0>
Hello, world.
Hello, world.
Hello, world.
Hello, world.
> P!stop.<0.59.0>, exit.
stop
>

●最後の例題:greetingプロセス

最後に次のようなプロセスを作ってみましょう。

  1. メッセージstartで挨拶の繰り返しをはじめる。
  2. メッセージpauseで挨拶を一時停止する。
  3. メッセージrestartで挨拶を再開する。
  4. メッセージfinishでプロセスを終了する。


greeting_main() ->
receive
start -> greeting_loop();
finish -> % はじまる前に終了
io:fwrite("~w, exit from main.\n", [self()]),
exit(ok);
_Other -> greeting_main() % その他のメッセージは無視
end.

greeting_loop() ->
io:fwrite("Hello, world.\n"),
receive
pause -> greeting_wait();
finish ->
io:fwrite("~w, exit from loop.\n", [self()]),
exit(ok);
_Other -> greeting_loop() % その他のメッセージは無視、即座に次の挨拶
after 3*1000 -> greeting_loop()
end.

greeting_wait() ->
receive
restart -> greeting_loop();
finish ->
io:fwrite("~w, exit from wait.\n", [self()]),
exit(ok);
_Other -> greeting_wait() % その他のメッセージは無視
end.

greeting() ->
spawn(hello, greeting_main, []).


> P = hello:greeting().<0.183.0>
> P!start.
Hello, world
start
Hello, world
Hello, world
> P!pause.
pause
> P!restart.
Hello, world
restart
Hello, world
Hello, world
Hello, world
> P!finish.<0.183.0>, exit from loop.
finish
>

うーん、こんなんでいいんかいね? まっ、動いているみたいよ、とりあえず。

[追記]調べてみたら、状態遷移を繰り返してもスタックの消費はないようですが、無関係(未定義)なメッセージがキューにたまってしまいます。その他のメッセージを無視するように手直しました。が、挨拶の実行中(greeting_loop内)に到着した無関係メッセージに反応すると、3秒ごとのタイミングは守れなくなります。それでいいとしましょう(中途半端ーッ!)。[/追記]

werlを使うと、プログラムからの出力とキーボード入力が混じらないでチャント操作できます。erlでも、待ち時間 3*1000 を長くすれば十分に操作できるでしょう(プロセスの出力を別ウィンドウに向けられるといいんですが、どうするか分かりません)。

ランタイム内のプロセス状況を見るには、psコマンドのようなi()があります、特定のプロセスの情報を得るには、i(0, 183, 0)のようにします。iコマンドの引数にPIDを入れてもダメなようです。

●もう一度、Erlangって何がいいのか?

Erlangが汎用言語として使いやすいとはとても思えないのですが、軽量プロセスを使った並列処理や、ノード(ネットワーク上のランタイム)間通信による分散処理などが、比較的容易に書けるのが魅力です。単に新参の(実際は古いのだけど)関数型言語として言語仕様を横並びで比較されるちゃうと、ちょっと見劣りする気がするな。Erlangの独特な個性のほうに注目すべきでしょうね、やっぱり。

*1:Prologの関数のようなものは、真偽値(成功または失敗)しか返せないので、一般的な戻り値は、出力用引数変数に代入して返します。例では、第3引数が出力変数。もっとも、入力変数と出力変数の区別がないのがPrologのウリだったりもするが。

*2:ちなみに、appendに相当する++って演算子が組み込みであります。

*3:[追記]どうも適切な呼び名じゃないと思うのだけど、Erlangではモジュールの集まりを何であれ「アプリケーション」というらしい。これ、普通の感覚のアプリケーションじゃないですよ、「kernelアプリケーション」とか「STDLIBアプリケーション」とか。[/追記]

*4:型をアノテーションとして付加するtyperってコマンドがErlang配布に付属してます。

*5:CTRL-Gで中断できれば、実はそこからプロセスをkillできます。