Displaying posts written in

3月 2009

外部イテレータとか内部イテレータとか

先日示したコード

play_songs(Socket, Pid, I) ->
    {Bin, Header} = rpc(Pid, next_block),
    write_data(Socket, Bin, {I, Header}),
    play_songs(Socket, Pid, I+1).

これは形式的には「外部イテレータ」のように見える。

そして「内部イテレータ」の形式に書き換えることもできる。

play_songs(Socket) ->
   each_song_block(fun(Bin, Header, I) ->
                            write_data(Socket, Bin, {I, Header}),
                            I+1
                    end, 0).

この場合は別プロセスを立ち上げる必要は無い。逆に考えると、プロセスとメッセージパッシングの考え方を用いることで、内部イテレータ(的な処理)を外部イテレータ(的な処理)に書き換えることができる……と言えそうである。

内部イテレータを外部イテレータに、という話題では、確か「コルーチン」が云々という議論があったはず……と思って検索してみたら、案の定興味深いページがいろいろと見つかった。

マルチプロセスとメッセージパッシングによる真の並列性を備えたErlangならば、コルーチンと同等の構造も容易に作れる……という理解で良いのだろうか。

14章SHOUTcastサーバのリファクタリング

14.7の『SHOUTcastサーバ』はこの本の中で2番目の山場だと思う。send_file/5 とか、処理の見通しが悪過ぎて投げ出したくなった……。が、頑張ってなんとか読み進めている。

まず「ファイルの中の、ある範囲内にランダムアクセスする」処理を分離したい。というわけで次のようなモジュールを書いた。一見して明らかなように、露骨にOOを意識している。

-module(filerange).
-export([open/2, close/1, pread/3]).
-record(file_with_range, { io, min, max }).
 
open(File, Min, Max) ->
    {ok, Io} = file:open(File, [read, binary, raw]),
    #file_with_range{io=Io, min=Min, max=Max}.
 
close(#file_with_range{io=Io}) ->
    file:close(Io).
 
pread(#file_with_range{io=Io, min=Min, max=Max}, Offset, Size) ->
    Start = Min + Offset,
    End   = Start + Size,
    if
	End > Max ->
	    case file:pread(Io, Start, Max - Start) of
		{ok, Data} -> {less, Data};
		eof -> {less, <<>>}
	    end;
	true ->
	    {ok, Data} = file:pread(Io, Start, Size),
	    {exact, Data}
    end.

しかしこれではイマイチだ。グローバルなモジュールの名前空間を占有する割には、さほど汎用的なコードではない。というか別に汎用的にするつもりもない。単に処理のスコープを分割したいだけだ。しかしそういう気軽な分割に用いるには、モジュールという単位は大げさ過ぎる……。

そこでふと、以前に「様々なプログラミングパラダイムを全てプロセスとメッセージ送信に還元していくのがErlang流だ」と考えたことを思い出した。そうだ、これも別プロセスで動かしてしまおう!

filerange_server(File, Min, Max) ->
    {ok, Io} = file:open(File, [read, binary, raw]),
    filerange_loop(Io, Min, Max).
 
filerange_loop(Io, Min, Max) ->
    receive
	{From, {pread, Offset, Size}} ->
	    Start = Min + Offset,
	    End   = Start + Size,
	    if
		End > Max ->
		    case file:pread(Io, Start, Max - Start) of
			{ok, Data} -> From ! {self(), {less, Data}};
			eof        -> From ! {self(), {less, <<>>}}
		    end;
		true ->
		    {ok, Data} = file:pread(Io, Start, Size),
		    From ! {self(), {exact, Data}}
	    end,
	    filerange_loop(Io, Min, Max);
	{From, close} ->
	    From ! {self(), file:close(Io)}
    end.

これで名前空間は適切に分割され、汎用の rpc で呼び出せるようになった。デバッグも簡単だ。

erl> Pid = spawn(fun() -> myshout:filerange_server("AtoZ.txt", 5, 20) end).
<0.37.0>

erl> myshout:rpc(Pid, {pread, 0, 5}).
{exact,<<"FGHIJ">>}

erl> myshout:rpc(Pid, {pread, 10, 5}).
{exact,<<"PQRST">>}

erl> myshout:rpc(Pid, {pread, 0, 100}).
{less,<<"FGHIJKLMNOPQRST">>}

さらに調子に乗って play_songs/3 の中の曲選択部分も別プロセスとして分離した結果、「曲データを無限に読み出し続ける」という処理は次のように簡潔な記述になった。

play_songs(Socket, Pid, I) ->
    {Bin, Header} = rpc(Pid, next_block),
    write_data(Socket, Bin, {I, Header}),
    play_songs(Socket, Pid, I+1).

いい感じだ。何となくErlangのプロセスの面白さが分かってきた気がする。