今回は Erlang のファイル入出力について説明します。Erlang は「ポート (port)」というデータ型を介して外部と通信処理を行います。ファイルの入出力処理もポートを介して行われますが、モジュール io, file などに用意されている関数を使うと、ポートを意識しないで入出力処理を行うことができます。
通常のファイルは、ファイルに対応するポートを生成しないとアクセスすることはできません。ただし、標準入出力は Erlang を起動した時から簡単に利用することができます。一般に、キーボードからの入力を「標準入力」、画面への出力を「標準出力」といいます。
データの入出力処理は標準入出力を使うと簡単です。モジュール io には標準入出力用の関数が用意されています。たとえば、io:write や io:format はデータを標準出力へ出力する関数でした。関数 io:read/1 は Erlang で取り扱うことができる「項」を入力することができます。
io:read(Prompt) => {ok, Data} | eof | {error, Reason}
引数 Prompt は画面に表示するプロンプトで、文字列かアトムで指定します。省略することはできません。read はファイルの終了を検出すると eof を返します。エラーが発生した場合は {error, Reason} を返します。
簡単な実行例を示しましょう。
> io:read(">> ").
>> 1234.
{ok,1234}
> io:read(">> ").
>> 1.2345.
{ok,1.2345}
> io:read(">> ").
>> foo.
{ok,foo}
|
> io:read(">> ").
>> [1,2,3,4].
{ok,[1,2,3,4]}
> io:read(">> ").
>> {a, b, c, d}.
{ok,{a,b,c,d}}
> io:read(">> ").
>> "hello, world".
{ok,"hello, world"}
|
データの終わりにはピリオドを入力してください。read は Erlang の構文規則にしたがって標準入力からデータを読み込み、それを項に変換して返します。
標準入力から 1 行ずつ読み込む場合、関数 io:get_line/1 を使うと便利です。
io:get_line(Prompt) => Data | eof | {error, Reason}
引数 Prompt は画面に表示するプロンプトで、文字列かアトムで指定します。省略することはできません。
簡単な実行例を示します。
> io:get_line(">> ").
>> hello, world
"hello, world\n"
hello, world と入力してリターンキーを押すと、get_line は入力データを文字列 (リスト) にして返します。このとき、改行文字もいっしょに文字列に格納されます。また、get_line はファイルの終了を検出すると eof を返します。エラーが発生した場合は {error, Reason} を返します。
それでは簡単な例題として、入力をそのままエコーバックする関数 echo/0 を作ってみましょう。プログラムは次のようになります。
リスト : エコーバック
-module(iotest).
-export([echo/0]).
echo() ->
case io:get_line("> ") of
"\n" -> ok;
Line -> io:fwrite(Line), echo()
end.
標準入力から get_line で 1 行読み込み、それを io:fwrite で標準出力へ出力します。fwrite は io:format と同じ働きをする関数です。第 1 引数に文字列を与えると、それをそのまま出力します。それから echo を再帰呼び出します。これで echo は無限ループになります。入力された文字列が "\n" の場合は処理を終了します。
簡単な実行例を示します。
> iotest:echo(). > hello, world hello, world > foo bar baz foo bar baz > 1234567890 1234567890 > ok
終了する場合はリターンキーだけを押してください。最後の ok は echo/0 の返り値です。
ファイルにアクセスする場合、次の 3 つの操作が基本になります。
「ファイルをオープンする」とは、アクセスするファイルを指定して、それと 1 対 1に対応するポートを生成することです。入出力関数はオープンしたポートを経由してファイルにアクセスします。Erlang の場合、ファイルのオープンは関数 file:open/2 で行います。
file:open(Filename, Mode) => {ok, IoDevice} | {error, Reason}
open/2 は引数にファイル名 Filename とアクセスモード Mode を指定して、Filename で指定されたファイルに対応する IoDevice を生成して返します。Erlang ではファイルの入出力処理をプロセスで行っていて、open はそのファイルの入出力を行うプロセスの識別子 (Pid) を返します。このほかに「ファイルディスクプリタ」という IoDevice もありますが、本稿では触れません。
アクセスモード Mode はリストで、要素はアクセスモードを表すアトムになります。要素がひとつしかない場合、リストではなくアトムをそのまま指定してもかまいません。下表に主なアクセスモードを示します。
| モード | 動作 |
|---|---|
| read | 読み込み (read) モード |
| write | 書き出し (write) モード |
| append | 追加 (append) モード |
| binary | バイナリの指定 |
読み込みモードの場合、ファイルが存在しないとエラーになります。書き出しモードの場合、ファイルが存在すれば、そのファイルを大きさ 0 に切り詰めてからオープンします。追加モードの場合、ファイルの最後尾にデータを追加します。binary は取り扱うデータをバイナリデータに設定します。
なお、Windows では「テキストモード」と「バイナリモード」の区別がありますが、Erlang の基本はバイナリモードです。Erlang の場合、行単位で入出力を行う関数において改行の変換が行われます。
オープンしたファイルは必ずクローズしてください。この操作を行う関数が file:close/1 です。
file:close(IoDevice) => ok | {error, Reason}
ファイルの読み込みはモジュール file に定義されている関数のほかに、モジュール io の関数でも第 1 引数に IoDevice を指定すると利用することができます。主な入力関数を以下に示します。
io:read(IoDevice, Prompt) => Result
io:get_line(IoDevice, Prompt) => Data | eof | {error, Reason}
file:read_line(IoDevice) => {ok, Data} | eof | {error, Reason}
file:read(IoDevice, Number) => {ok, Data} | eof | {error, Reason}
file:read_file(Filename) => {ok, Binary} | {error, Reason}
read_line/1 は IoDevice から1 行読み込みます。Windows の場合、改行コードの変換が行われます。file:read/2 は Number で指定した個数だけデータを読み込みます。返り値の Data は、open の Mode で binary を指定するとバイナリに、そうでなければリストになります。read_file/1 は Filename で指定したファイルを全部読み込み、データをバイナリに格納して返します。
簡単な例を示します。ファイル test.dat からデータを読み込みます。
foo bar baz 1234 5678 図 : test.dat の内容
> file:read_file("test.dat").
{ok,<<"foo\r\nbar\r\nbaz\r\n1234\r\n5678\r\n">>}
> {ok, F} = file:open("test.dat", read).
{ok,<...>}
> file:read_line(F).
{ok,"foo\n"}
> file:read_line(F).
{ok,"bar\n"}
> file:read_line(F).
{ok,"baz\n"}
|
> file:read_line(F).
{ok,"1234\n"}
> file:read_line(F).
{ok,"5678\n"}
> file:read_line(F).
eof
> file:close(F).
ok
|
read_file/1 を使って test.dat を読み込むと、データを格納したバイナリが返されます。M.Hiroi の実行環境は Windows なので、改行が \r\n で表されていることがわかります。次に、read_line/1 で 1 行ずつ読み込みます。open のモードで binary を指定していないので、データはリストに格納されて返されます。このとき、改行の変換が行われていることに注意してください。
次は open のモードで binary を指定してみましょう。
> {ok, F1} = file:open("test.dat", [read, binary]).
{ok,<...>}
> file:read_line(F1).
{ok,<<"foo\n">>}
> file:read_line(F1).
{ok,<<"bar\n">>}
> file:read_line(F1).
{ok,<<"baz\n">>}
> file:read_line(F1).
{ok,<<"1234\n">>}
> file:read_line(F1).
{ok,<<"5678\n">>}
> file:read_line(F1).
eof
> file:close(F1).
ok
この場合、読み込んだデータはバイナリに格納されて返されます。行単位の入力なので、改行の変換も行われていることに注意してください。
次は file:read/2 を使って 6 バイトずつデータを読み込んでみましょう。
> {ok, F2} = file:open("test.dat", read).
{ok,<...>}
> file:read(F2, 6).
{ok,"foo\r\nb"}
> file:read(F2, 6).
{ok,"ar\r\nba"}
> file:read(F2, 6).
{ok,"z\r\n123"}
> file:read(F2, 6).
{ok,"4\r\n567"}
> file:read(F2, 6).
{ok,"8\r\n"}
> file:read(F2, 6).
eof
> file:close(F2).
ok
open のモードで binary を指定していないので、読み込んだデータはリストに格納されて返されます。file:read/2 では改行の変換が行われないことに注意してください。また、ファイルの最後では、データが足りなくて指定したサイズに満たない場合もあります。
それでは簡単な例題として、ファイルの内容を画面へ出力する関数 cat/1 を作ってみましょう。プログラムは次のようになります。
リスト : ファイルの表示
cat_sub(In) ->
case file:read_line(In) of
eof -> ok;
{ok, Line} -> io:fwrite(Line), cat_sub(In)
end.
cat(Filename) ->
case file:open(Filename, read) of
{error, Reason} -> io:format('file open error ~w~n', [Reason]);
{ok, In} -> cat_sub(In), file:close(In)
end.
cat/1 の引数 Filename はファイル名を表す文字列です。ファイル Filename をオープンして IoDevice を変数 In にセットします。ファイルの表示は関数 cat_sub/1 で行います。read_line/1 で 1 行読み込み、それを fwrite/1 で標準出力へ出力します。それから cat_sub/1 を再帰呼び出しします。ファイルの終了を検出したら処理を終了します。あとは cat/1 に戻って、close でファイルを閉じるだけです。
簡単な実行例を示します。
> iotest:cat("test.dat").
foo
bar
baz
1234
5678
ok
データをファイルに書き込むには、ファイルを write モードでオープンします。このとき、注意事項が一つあります。既に同じ名前のファイルが存在している場合は、そのファイルの長さを 0 に切り詰めてからデータを書き込みます。既存のファイルは内容が破壊されることに注意してください。
モジュール io と file に定義されている主な出力関数を以下に示します。
io:nl(IoDevice) => ok
io:write(IoDevice, Term) => ok
io:format(IoDevice, Format, Args) => ok
file:write(IoDevice, Data) => ok | {error, Reason}
file:write_file(Filename, Data) => ok | {error, Reason}
Data はリストまたはバイナリで、要素の値はバイナリデータ (0 - 255) になります。なお、リストの要素はバイナリデータを格納したリストまたはバイナリでもかまいません。
簡単な例を示します。
> {ok, Out1} = file:open("test.out", write).
{ok,<...>}
> file:write(Out1, "foo").
ok
> file:write(Out1, "bar").
ok
> file:write(Out1, "baz").
ok
> file:write(Out1, ["1234", "5678"]).
ok
> file:close(Out1).
ok
> {ok, Data} = file:read_file("test.out").
{ok,<<"foobarbaz12345678">>}
> file:write_file("test1.out", Data).
ok
> file:read_file("test1.out").
{ok,<<"foobarbaz12345678">>}
ファイル test.out を write モードでオープンします。write でデータを書き込みますが、このときデータにリストを渡すと、格納された文字列をファイルに書き込みます。test.out をクローズしたあと、read_file/1 で test.out を読み込むと、データが書き込まれていることがわかります。それから、そのデータを write_file/1 でファイル test1.out に書き込みます。read_file/1 で test1.out を読み込むと、正常に書き込まれていることがわかります。
このほかにも、Erlang にはファイルを操作する便利な関数が多数用意されています。詳しい説明は Erlang のリファレンスマニュアル Erlang -- io, Erlang -- file をお読みください。