今回は 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 をお読みください。