先日示したコード
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.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のプロセスの面白さが分かってきた気がする。