Displaying posts tagged with

“『プログラミングErlang』”

IRC Liteのコードを丁寧に読む

パッと見の印象ほど複雑ではないし、難しくもない(入門書の例なんだから当然か)。いくつか明文化されていない設計方針を読み取れれば、かなり見通しがよくなる。

状態遷移モデル

オートマトンがどうとか理論的なことはよく分からないが、「状態」と「状態遷移」の考え方が使われていることは素朴に理解できる。状態は、全体が receive … end で構成された関数として表現されており、メッセージの受信を引き金として別の関数を呼び出す(=遷移)か、末尾再帰で自分自身を呼び出す(=遷移無し)、もしくは終了する。

状態を表す関数は名前も特徴的である。普通の関数は「動詞+目的語」が多いが、状態の関数は「形容詞または*loop」の形をしている。

この状態遷移モデルについて、本文中では11.3のチャットクライアントの説明の中でわずかに示唆されている。どうせなら状態遷移図でも載せておけばより分かりやすかったのではないかと思うのだが、説明が煩雑になることを避けたのか、あるいは後々OTPを用いる際には不要になる概念なのか……今の段階ではまだ分からない。

オブジェクト指向

状態遷移モデルにおける「状態」の関数は、オブジェクト指向における「オブジェクト」との類似性も併せ持っている。すなわちプロセスがインスタンスであり、ループの引数がインスタンス変数であり、応答するメッセージがメソッドである。io_widgetが特に分かりやすい。

この類似性は、ある意味当たり前ではある。なぜならメッセージ送信モデルはオブジェクト指向の先祖の一つだからだ。

……
状態遷移モデルにせよオブジェクト指向にせよ、様々なプログラミングパラダイムを全てプロセスとメッセージ送信に還元していくのがErlang流、という理解で良いのだろうか。

最後にコードの中に見つけた変なところを挙げておく。

  • io_widget:update_state/3 は不要。またio_widget:loopで{updateState}を受ける部分も不要
  • chat_group:delete/3 で、lists:reverse/2 の引数が逆(これだと削除するごとにリストが逆順になるので意味がない)
  • io_widget:widget/1 内のpackerの設定が変。maxよりminが大きかったり、minとmaxが同じだったり(fixedを使えば済む用途のはず)

第11章『IRC Lite』はこの本の最初の山場だと思う

どこまで突っ込むべきか、悩ましい。

  • lib_chanの実装にどこまで突っ込むか
  • GUIプログラミングにどこまで突っ込むか
  • 11.7の演習にどこまで付き合うか

lib_chanの詳細は本の末尾、付録Dで解説されている。だから当面は使い方だけ覚えて、実装については後回しで良い……と判断したいところなのだが、そうも言っていられない。11章までに載っている説明では、IRC Liteのコードを理解することはとてもできないからだ。

Erlangの標準GUIライブラリ gs については、公式サイトの『GS User’s Guide』が非常に分かりやすい。頭から順に目を通していって、最後の「Built In Objects」を必要に応じて参照すれば、十分に事足りると思われる。

11.7の演習には、問題の意図がよく分からないものがいくつかある。「1対1の会話を追加してみよう」。これはちょっと曖昧過ぎる。「グループコントローラを、グループに最初に入ったユーザのところで動かそう」。これはグループに最初に入ったユーザのマシンをサーバとして機能させる、という意味なのだろうか?もしそうなら、今とは全く異なるアーキテクチャを考えなければならない気がする……。それとも他に選択肢があるのか?

IRC Liteが動かない

『プログラミングErlang』11章のIRC Lite。オーム社のサイトからコードを落として実行しても、うまく動かなかった。以下、解決までの道のり。

lib_md5が無いというエラーになる

これは原著のフォーラムで解決策が見つかった。

getting error on starting chat-client: lib_md5 missing?

socket_distでmakeを実行する前に、その一つ上のディレクトリ(日本語版ならjaerlang-code-ja)でmakeを実行しなければならない。

io_widgetのウィンドウが表示されない(Ubuntuの場合)

tk がインストールされていないと動かない。

apt-get install tk

標準ではtk 8.4がインストールされる。

io_widgetのウィンドウが表示されない(Mac OS Xの場合)

MacPortsでErlangをインストールした場合、自動的にtk 8.5.5がインストールされる。しかし下のようなエラーが出て動かない。

Application initialization failed: couldn't connect to display ":0.0"
Error in startup script: couldn't connect to display ":0.0"
    while executing
"load /opt/local/lib/tk8.5/../libtk8.5.dylib Tk"
    ("package ifneeded Tk 8.5.5" script)
    invoked from within
"package require Tk 8.3"
    (file "/opt/local/lib/erlang/lib/gs-1.5.9/priv/gstk.tcl" line 7)

いろいろ検索した結果、X11.appを起動していないと動かないということに気付いた(「アプリケーション」→「ユーティリティ」→「X11.app」)。

MacPortsのtkには「quartz: Use native Mac OS X UI instead of X11」というvariantsがあって、これを有効にすればX11を起動しなくても動くかも?と思ったが、駄目だった。
……
いずれも初歩的なことなんだが、解決までにずいぶんと時間がかかってしまった。

『プログラミングErlang』8章 練習問題 答案

githubに登録して、『プログラミングErlang』8章11の練習問題2のコードを載せてみた。初git。

http://github.com/tkyk/erlang-exercise—/tree/master

8章までの知識と覚えたばかりのmakeの知識を総動員して、極力凝った構成にしてある。コードは役割ごとに複数のモジュールに分割して、特にリングを構築する部分は実行時にアルゴリズムを切り替えられるようになっている。いわゆるストラテジーパターン。

次のようにコードを落としてきてmakeを実行すると

git clone git://github.com/tkyk/erlang-exercise---.git
make

erlのコンパイルが行われた後、各アルゴリズムに対応した3つのシェルスクリプトが生成される。いずれも今まで日記に書いてきたもの。

引数としてN(プロセス数)とM(メッセージを回す数)を与えて実行すると、erlが起動されて計算が実行され、実行時間が表示される。また make run とすると3つのスクリプトそれぞれに、何通りかのNとMを与えて自動的に実行する。

$ ./ring_constructor_child.sh 1000 1000
Constructor=ring_constructor_child, N=1000, M=1000
time=0.55, (0.563) seconds

なお、この練習問題では「リング」の定義が曖昧なので、自分なりの判断で以下の条件を課している。

  1. メッセージの転送を開始できるのは始点となるプロセスのみ
  2. 始点以外のプロセスは自分の”前”のプロセスを厳密に認識して、他からのメッセージは受け取らない

この条件により実装が複雑になっている部分があるので、条件を外して単純化したバージョンを別ブランチで用意した。

http://github.com/tkyk/erlang-exercise—/tree/ignore_msg_sender

lib_ring.erl以外の内容はmasterブランチと全く同じで、使い方も同じで、実行速度もほとんど同じである。

……
この練習問題にここまでこだわる理由はどこにもないのだが、Erlangに飽きないうちにできるだけのことはやっておきたい。

選択受信を理解する

『プログラミングErlang』8.6で解説されている選択受信について、ちゃんと理解できているか自信がなくなってきたので、下のようなコードを書いて実験した。

loop(X) ->
    Y = X + 1,
    receive
        X ->
            io:format("received: ~p, continue.~n", [X]),
            loop(Y);
        Y ->
            io:format("received: ~p, finish.~n", [Y])
    end.

receiveのパターンにマッチしなかったメッセージは捨てられるわけではない。プロセスの「メールボックス」に保存されたまま照合の機会を待ち続ける。

9> Pid = spawn(fun() -> lib_misc:loop(1) end).
<0.49.0>
10> Pid ! 1.
received: 1, continue.
1
11> Pid ! 2.
received: 2, continue.
2
12> Pid ! 6.
6
% この時点ではマッチしない

13> Pid ! 3.
received: 3, continue.
3
14> Pid ! 4.
received: 4, continue.
4
received: 6, finish.
% 再び照合が行われ、マッチした!

死んだはずのプロセスからメッセージが届く

『プログラミングErlang』8章に載っている on_exit/2 を使うと、既に死んでいるプロセスから死亡通知メッセージが届く、ように見える。

1> Foo = fun() -> 1 end.
#Fun<erl_eval.20.67289768>
2> Pid = spawn(Foo).
<0.33.0>
3> erlang:is_process_alive(Pid).
false
4> lib_misc:on_exit(Pid, fun(Why) -> io:format("~p dead with:~p~n", [Pid, Why]) end).
<0.33.0> dead with:noproc
<0.36.0>

これは erlang:link/1 の性質によるもの。man erlang より引用。

* If the calling process is not trapping exits, and checking Pid is cheap — that is, if Pid is local — link/1
fails with reason noproc.

* Otherwise, if the calling process is trapping exits, and/or Pid is remote, link/1 returns true, but an exit sig-
nal with reason noproc is sent to the calling process.

link/1の呼び出し自体は成功する、すなわち正常にリンクが確立できたように見える、というところがポイント。

この性質のせいで 9.8 の「キープアライブプロセス」の説明がおかしなことになっている。理解するのに丸一日かかったよ……。

『プログラミングErlang』8章 練習問題(2)

昨日貼り付けたコードにはバグがあったので、公開後に一度修正した(loop/3)。その時にリングを構築するための別の方法も思いついたので、下に示しておく。一度にまとめてプロセスを作るのではなく、子が孫を生む方式。

construct(N) when N > 0 ->
    spawn(?MODULE, prepare, [N]).
 
prepare(1) ->
    lib_ring:entryloop(self());
prepare(N) ->
    Next = spawn(?MODULE, prepare, [self(), self(), N-1]),
    lib_ring:entryloop(Next).
 
prepare(First, Prev, 1) ->
    lib_ring:loop(Prev, First);
prepare(First, Prev, N) ->
    Next = spawn(?MODULE, prepare, [First, self(), N-1]),
    lib_ring:loop(Prev, Next).

これで実行時間はさらに短縮された。プロセス数が多くなるほど差が顕著になる。

辞書方式:

N=100000, M=100
time=8.65, (9.113) seconds
N=200000, M=10
time=6.41, (7.012) seconds

子孫方式:

N=100000, M=100
time=7.05, (7.573) seconds
N=200000, M=10
time=1.67, (2.005) seconds

『プログラミングErlang』8章 練習問題

8.11の練習問題。これ本の中には答え載ってないのかな…?

自分なりの回答。時間計測部分のコードは省略。

  • コードの簡潔さを優先してリングの”始点”は固定にした
  • リストを配列代わりに使っているので効率が悪い

(続きを読む…)

『プログラミングErlang』6章まで読んだ

やっと6章まで読み終わった。いよいよ7章から並行処理に入る。

ところで6章には「コマンドライン引数を使うプログラム」の例として階乗を計算するプログラムが載っているのだが、これをそのまま実行すると引数の数が合わない場合にクラッシュダンプを吐く。流石にもうちょっと何とかならないかと思って調べてみたところ、次のようなことが分かった:

  • コマンドライン引数の数によって呼び出される関数が変わる。erl -s module func と指定した場合、コマンドライン引数を一つも与えなかった場合は module:func/0 が、一つ以上与えた場合は module:func/1 が呼び出される。
  • module:func/1の引数はアトムのリストである。
  • escriptではこの差異を吸収して、アトムをリスト(文字列)に変換した上で main/1 を呼び出している。

ということで escript の動作にあわせて次のようにしてみた。

-module(fac2).
-export([run_main/0, run_main/1]).
 
run_main()  -> main([]).
run_main(L) -> main(lists:map(fun atom_to_list/1, L)).
 
main([A]) ->
    I = list_to_integer(A),
    F = fac(I),
    io:format("factorial ~w = ~w~n", [I, F]),
    init:stop();
main(_) ->
    io:format("Usage: ~p:rum_main <num>~n", [?MODULE]),
    init:stop().
 
fac(0) -> 1;
fac(N) -> N * fac(N-1).

起動は erl -noshell -s fac2 run_main とする。


それと、6章にはMakefieのテンプレートも載っているのだが、その中の .SUFFIXES および .erl.beam といったターゲット名は古い形式の定義らしい。新しい形式では型ルールを使って次のように書けば良いようだ。

%.beam: %.erl
erlc -W $<