OCaml は「チャネル (channel)」というデータ型を介して入出力処理を行います。チャネルは、いわゆるチャンネルのことで、もともとは水路とか海峡という意味ですが、コンピュータの世界では通信路とか伝送路の意味で使われています。
C言語や Common Lisp など近代的なプログラミング言語は「ストリーム (stream)」とういデータ型を介してデータの入出力を行います。OCaml のチャネルはストリームと同じものです。今回はファイルの入出力について簡単に説明します。
OCaml では、チャネルを介してファイルにアクセスします。チャネルはファイルと 1 対 1 に対応していて、ファイルからデータを入力するときは、チャネルを経由してデータが渡されます。逆に、ファイルへデータを出力するときもストリームを経由します。入力チャネル表すデータ型が in_channel で、出力チャネルを表すデータ型が out_channel です。
通常のファイルは、チャネルを生成しないとアクセスすることはできません。ただし、標準入出力は OCaml の起動時にチャネルが自動的に生成されるので、簡単に利用することができます。一般に、キーボードからの入力を「標準入力」、画面への出力を「標準出力」といいます。標準入出力に対応するチャネルは大域変数に格納されています。表 1 に変数名を示します。
変数名 | ファイル |
---|---|
stdin | 標準入力 |
stdout | 標準出力 |
stderr | 標準エラー出力 |
データの入出力処理は標準入出力を使うと簡単です。たとえば、print_string は文字列を標準出力へ出力する関数でしたが、入力チャネルから文字列を読み込む関数が read_line です。
val read_string : unit -> string = <fun>
# read_line ();; hello, world - : string = "hello, world"
hello, world と入力してリターンキーを押すと、read_line は入力データを文字列にして返します。このとき、改行文字が取り除かれることに注意してください。また、read_line はファイルの終了を検出すると例外 End_of_file を送出します。
それでは簡単な例題として、入力をそのままエコーバックする関数 echo を作ってみましょう。プログラムは次のようになります。
リスト 1 : エコーバック let echo () = let rec echo_sub () = print_string (read_line ()); print_newline (); echo_sub () in try echo_sub () with End_of_file -> ()
実際の処理は局所関数 echo_sub で行っています。標準入力から read_line で 1 行読み込み、それを print_string で標準出力へ出力します。改行は取り除かれているので、print_newline で改行を付け加えます。なお、OCaml には文字列と改行を出力する関数 print_endline も用意されているので、そちらを使ったほうが簡単でしょう。
あとは echo_sub を再帰呼び出しするだけですが、ファイルの終了時には例外 End_of_file が送出されるので、それを try 式で受け取ります。echo_sub は再帰呼び出しの停止条件がない、つまり「無限ループ」になっているので、例外を送出しないとプログラムを停止できないことに注意してください。
簡単な実行例を示します。
val echo : unit -> unit = <fun>
# echo ();; abcd <-- 入力 abcd efgh <-- 入力 efgh hello, world <-- 入力 hello, world
Unix 系 OS の場合、echo を終了するには Ctrl-D を入力してください。
ファイルにアクセスする場合、次の 3 つの操作が基本になります。
「ファイルをオープンする」とは、アクセスするファイルを指定して、それと 1 対 1に対応するチャネルを生成することです。入出力関数はオープンしたチャネルを経由してファイルにアクセスします。
OCaml の場合、ファイルをオープンするには関数 open_in と open_out を使います。オープンしたファイルは必ずクローズしてください。この操作を行う関数が close_in と close_out です。
val open_in : string -> in_channel = <fun> val open_out : string -> out_channel = <fun>
val close_in : in_channel -> unit = <fun> val close_out : out_channel -> unit = <fun>
ファイル名は文字列で指定し、ファイル名のパス区切り記号にはスラッシュ ( / ) を使います。\ は文字列のエスケープコードに割り当てられているため、そのままではパス区切り記号に使うことはできません。ご注意くださいませ。また、ファイルのオープンやクローズに失敗した場合は例外 Sys_error が送出されます。
OCaml で用意されている、主な入出力関数を次に示します。
読み込み val intput_char : in_channel -> char = <fun> val intput_line : in_channel -> string = <fun> val intput_byte : in_channel -> int = <fun>
書き込み val output_char : out_channel -> char -> unit = <fun> val output_line : out_channel -> string -> unit = <fun> val output_byte : out_channel -> int -> unit = <fun>
関数 input_char は入力チャネルから 1 文字 (1 byte) 読み込みます。関数 input_line は入力チャネルから 1 行読み込みます。このとき、改行は削除されます。関数 intput_byte は入力チャネルから 1 バイト読み込みます。返り値は整数 (0 - 255) になります。ファイルの終了を検出すると、これらの入力関数は例外 End_of_file を送出します。
関数 output_char は出力チャネルに 1 文字 (1 byte) 書き込みます。関数 output_line は出力チャネルに 1 行書き込みます。関数 output_byte は整数 (n mod 256) を出力チャネルに書き込みます。
それでは簡単な例題として、ファイルの内容を画面へ出力する関数 cat を作ってみましょう。プログラムは次のようになります。
リスト 2 : ファイルの表示 (1) let cat filename = let fin = open_in filename in let rec cat_sub () = output_char stdout (input_char fin); cat_sub () in try cat_sub () with End_of_file -> close_in fin
関数 cat の引数 filename はファイル名を表す文字列です。failename をオープンして入力チャネルを変数 fin にセットします。ファイルの表示は局所関数 cat_sub で行います。input_char で 1 文字読み込み、それを output_char で標準出力へ出力します。この処理は関数 print_char を使ってもかまいません。あとは cat_sub を再帰呼び出しします。
cat_sub には再帰呼び出しの停止条件がないので、この処理は「無限ループ」になることに注意してください。cat では cat_sub を呼び出して、例外 End_of_file を try 式で受け取ります。そして、close_in で入力チャネル fin をクローズします。
ところで、cat は再帰呼び出しを使いましたが、繰り返しでも簡単にプログラムを作ることができます。次のリストを見てください。
リスト 3 : ファイルの表示 (2) let cat1 filename = let fin = open_in filename in let cat_sub () = while true do output_char stdout (input_char fin) done in try cat_sub () with End_of_file -> close_in fin
cat_sub では while の条件式に true を指定して「無限ループ」を構成していることに注意してください。例外を使ってファイルの終了をチェックしているので、プログラムはとても簡単になります。
なお、input_char と output_char のかわりに input_line と output_line を使って行単位で入出力を行っても同じようにプログラムを作ることができます。
データをファイルに書き込むには、ファイルを open_out でオープンします。このとき、注意事項が一つあります。既に同じ名前のファイルが存在している場合は、そのファイルの長さを 0 に切り詰めてからデータを書き込みます。既存のファイルは内容が破壊されることに注意してください。
それでは簡単な例題として、string list の要素を 1 行ずつファイルに書き込む関数 output_stringlist を作ってみましょう。次のリストを見てください。
リスト 4 : ファイルの書き込み let output_stringlist filename xs = let fout = open_out filename in List.iter (fun x -> output_string fout (x ^ "\n")) xs; close_out fout
最初に open_out でファイル filename をオープンします。あとは、高階関数 List.iter を使ってリスト xs から要素を一つずつ取り出し、改行文字を付加してから output_string で出力するだけです。
このほかにも、OCaml にはいろいろな入出力関数が用意されています。詳しい説明は OCaml のリファレンスを参照してください。
次の関数を定義してください。
エントロピーについては拙作のページ Algorithms with Python: 「シャノン・ファノ符号とハフマン符号」をお読みくださいませ。
リスト : ファイルの先頭 10 行を表示する let head_file filename = let fin = open_in filename in let head_file_sub () = for i = 1 to 10 do print_endline (input_line fin) done; close_in fin in try head_file_sub () with End_of_file -> close_in fin
open_in で引数 filename をリードオープンします。あとは for ループ で 10 行読み込んで、print_endline で出力します。途中でファイルの終了を検出した場合は例外 End_of_file が送出されるので、それを try 式で捕捉して close_in でファイルをクローズします。
リスト : ファイルの末尾 10 行を表示する let tail_file filename = let fin = open_in filename in let buff = ref [] in let rec tail_file_sub () = let xs = input_line fin in if List.length !buff < 10 then buff := !buff @ [xs] else buff := (List.tl !buff) @ [xs]; tail_file_sub () in try tail_file_sub () with End_of_file -> close_in fin; List.iter print_endline !buff
読み込んだ直近の 10 行を変数 buff のリストに保持します。buff の長さが 10 に満たない場合、buff の末尾に読み込んだ行 [xs] を連結します。buff の長さが 10 の場合は、List.tl で先頭要素を取り除いてから [xs] を連結します。ファイルの終了を検出したら List.iter で buff に格納されている行を print_endline で出力します。
なお、このプログラムはリストの連結に @ (append) を使っているので、効率はよくありません。興味のある方はプログラムを改良してみてください。
リスト : ファイルを行単位で連結する let input_one_line fin = try Some (input_line fin) with End_of_file -> close_in fin; None let rec flush_file fin = match input_one_line fin with None -> () | Some x -> print_endline x; flush_file fin let paste_file file1 file2 = let fin1 = open_in file1 in let fin2 = open_in file2 in let rec paste_sub () = let buff1 = input_one_line fin1 in let buff2 = input_one_line fin2 in match (buff1, buff2) with (None, None) -> () | (Some x, None) -> print_endline x; flush_file fin1 | (None, Some x) -> print_endline x; flush_file fin2 | (Some x, Some y) -> print_string x; print_endline y; paste_sub () in paste_sub ()
最初に、引数 file1 と file2 を open_in でリードオープンします。次に関数 input_one_line で 1 行読み込み、変数 buff1 と buff2 にセットします。input_one_line は fin から 1 行読み込み、それを Some に包んで返します。ファイルの終了を検出した場合は、close_in でファイルをクローズしてから None を返します。
次に、match で buff1 と buff2 をパターンマッチングします。どちらも None であればユニット () を返します。(Some x, None) とマッチングしたならば、print_endline で x を出力し、flush_file で fin1 を出力します。逆に、(None, Some x) とマッチングした場合は fin2 を出力します。(Some x, Some y) とマッチングした場合、x と y を出力してから paste_sub を再帰呼び出しします。
リスト : ファイルのエントロピーを求める let make_frequency filename = let fin = open_in filename in let freq = Array.make 256 0 in let rec make_freq () = let c = input_byte fin in freq.(c) <- freq.(c) + 1; make_freq () in try make_freq () with End_of_file -> close_in fin; freq let entoropy filename = let freq = make_frequency filename in let sum = Array.fold_left (+) 0 freq in let e = -. (Array.fold_left (fun a x -> if x > 0. then a +. (x *. (log x /. log 2.)) else a) 0. (Array.map (fun x -> (float_of_int x) /. (float_of_int sum)) freq)) in (sum, e)
関数 make_frequency で記号 (0 - 255) の出現頻度表を作成します。ファイル filename を close_in でリードオープンし、input_byte で 1 バイトずつ読み込みます。あとは記号 c に対して frerq.(c) の値を +1 するだけです。関数 entoropy は各記号の出現確率 p を Array.map で求め、エントロピー e を Array.fold_left で計算します。
それでは、実際に Canterbury Corpus で配布されているテストデータ The Canterbury Corpus のエントロピーを求めてみましょう。
リスト : entoropy のテスト let test_entoropy () = let files = ["alice29.txt"; "asyoulik.txt"; "cp.html"; "fields.c"; "grammar.lsp"; "kennedy.xls"; "lcet10.txt"; "plrabn12.txt"; "ptt5"; "sum"; "xargs.1"] in List.iter (fun name -> let (s, e) = entoropy name in Printf.printf "%14s %8d %f %6.0f\n" name s e ((float_of_int s) *. e /. 8.)) files
関数 Printf.printf で結果を出力します。モジュール Printf にはC言語の標準ライブラリ関数 printf に相当する書式出力関数が定義されています。機能はC言語の printf とほぼ同じです。詳細は OCaml のリファレンスマニュアルをお読みください。
# test_entoropy ();; alice29.txt 152089 4.567680 86837 asyoulik.txt 125179 4.808116 75234 cp.html 24603 5.229137 16082 fields.c 11150 5.007698 6979 grammar.lsp 3721 4.632268 2155 kennedy.xls 1029744 3.573471 459970 lcet10.txt 426754 4.669118 249071 plrabn12.txt 481861 4.531363 272936 ptt5 513216 1.210176 77635 sum 38240 5.328990 25473 xargs.1 4227 4.898432 2588 - : unit = ()
各列の項目はファイル名、ファイルサイズ、エントロピー、下限値です。ファイルサイズ * エントロピー / 8 で圧縮の下限値を計算することができます。ただし、この結果は無記憶情報源モデルの場合であり、モデル化によってエントロピーの値は異なることに注意してください。