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

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

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

参照用 記事

Erlang実験室:状態遷移を書くのはこんなに簡単

あまり強調されないようですが、Erlangでは、その構文と実行メカニズムとがあいまって、状態遷移のプログラミングがとても容易です。

例題として、正規表現 /aa?b*c/ とマッチする文字列を認識するオートマトンを作ってみましょう。まず、状態遷移図を描き*1、それから遷移表を書きます。図と表のなかで、EOSは End Of String のマーカー、◎は終状態です

遷移表: 0から3までの各状態について、入力ごとの遷移先は次の通り。×はエラーです。

状態 文字a 文字b 文字c EOS その他
0 1 × × × ×
1 2 2 3 × ×
2 × 2 3 × ×
3 × × × ×

この表を見ながら、Erlangコードを書きます。以下のような感じ。


accept(N) -> % 引数Nが状態
case N of
0 ->
receive
$a ->
accept(1);
_ ->
exit(error)
end;
1 ->
receive
$a ->
accept(2);
$b ->
accept(2);
$c ->
accept(3);
_ ->
exit(error)
end;
2 ->
receive
$b ->
accept(2);
$c ->
accept(3);
_ ->
exit(error)
end;
3 ->
receive
0 -> % End Of String
exit(normal);
_ ->
exit(error)
end
end.

Erlangでは、文字'a'を $a と書きます(文字番号の整数値に過ぎないけど)。End Of Stringマーカーには数値0を使いました。_(アンダスコア)は「その他の入力」の意味です。オートマトンによる受理が成功すればnormalでプロセスを終了し、失敗ならerrorでプロセスを終了します。あまりにも表のまんまなんで笑っちゃうでしょ。

オートマトンの本体はとても簡単ですが、フロントエンドのプロセスのほうが少しめんどうです。次のようにしてみました(一例)。関数acceptを走らせるプロセスをアクセプターと呼ぶことにして、フロントのプロセスは、文字列を受け取ったらアクセプターを起動して文字を順にアクセプターに送りつけます。


% -*- coding: utf-8 -*-
-module(accept).
-compile(export_all).

start() ->
spawn(fun () -> process_flag(trap_exit, true), main_loop() end).

main_loop() ->
receive
stop ->
io:fwrite("byebye.~n"),
exit(normal);
Str when is_list(Str) -> % 文字列はアクセプターで処理する
Pid = spawn_link(?MODULE, accept, [0]), % アクセプターを起動
send_loop(Pid, Str, send); % アクセプターとの通信に入る
% 終われば main に戻ってくる(ご安心を)
_Other -> % ノイズは捨てる
main_loop()
end.

send_loop(Pid, [], send) ->
Pid ! 0, % アクセプターに End-Of-String を送る
send_loop(Pid, [], wait); % 待ちに入る
send_loop(Pid, [], wait) ->
receive
{'EXIT', Pid, error} -> % 失敗
io:fwrite("ERROR.~n"),
main_loop(); % 戻る
{'EXIT', Pid, normal} -> % 成功
io:fwrite("ACCEPTED.~n"),
main_loop(); % 戻る
_Other -> % ノイズは捨てる
send_loop(Pid, [], wait)
after 1000 -> % いくら何でも遅すぎるだろうよ
erlang:error(something_wrong)
end;
send_loop(Pid, [Ch|Rest], send) ->
receive
{'EXIT', Pid, error} -> % 失敗
io:fwrite("ERROR.~n"),
main_loop(); % 戻る
_Other -> % ノイズは捨てる
send_loop(Pid, [Ch|Rest], send)
after 0 ->
Pid ! Ch, % アクセプターに1文字送る
send_loop(Pid, Rest, send) % 残りの仕事
end.

accept(N) ->
% ...
% ...
end.

アクセプターの終了をフロントのプロセスで監視することによって、文字列が受理されたか否かを判定します。


1> c(accept).
{ok,accept}
2> P = accept:start().<0.37.0>
3> P!"abc".
ACCEPTED.
"abc"
4> P!"aabc".
ACCEPTED.
"aabc"
5> P!"aaabc".
ERROR.
"aaabc"
6> P!"axbc".
ERROR.
"axbc"
7> P!"abbc".
ACCEPTED.
"abbc"
8> P!stop.
byebye.
stop
9>

文字列の受理は、プロセスを使わずに関数だけでも当然できますが、状態遷移機械をプログラムすることは多いので、プロセスとメッセージによる状態遷移の実装方法を知っておいて損はありません。

*1:簡単な正規表現に対する遷移図は勘でも描けますが、僕はよく間違えます(苦笑)。ちゃんとした手順を踏んだほうが安全ですね。