M.Hiroi's Home Page

お気楽 Standard ML of New Jersey 入門

ファイル入出力

Copyright (C) 2005-2020 Makoto Hiroi
All rights reserved.

はじめに

SML/NJ の入出力は Common Lisp などの近代的なプログラミング言語と同様に「ストリーム (stream)」を介して行われます。入力ストリームを表すデータ型が instream で、出力ストリームを表すデータ型が outstream です。テキストファイルの入出力関数はストラクチャ TextIO にまとめられています。今回はファイルの入出力について説明します。

●ストリーム

ファイルからデータを入力、逆にデータをファイルへ出力する場合、SML/NJ では「ストリーム (stream)」を使います。ストリームは「流れ」や「小川」という意味ですが、プログラミング言語の場合は「ファイルとプログラムの間でやりとりされるデータの流れ」という意味で使われています。

SML/NJ では、ストリーム型データを介してファイルにアクセスします。ストリームはファイルと一対一に対応していて、ファイルからデータを入力する場合は、ストリームを経由してデータが渡されます。逆に、ファイルへデータを出力するときも、ストリームを経由して行われます。

●ファイルのオープンとクローズ

ファイルにアクセスする場合、次の 3 つの操作が基本になります。

  1. アクセスするファイルをオープンする
  2. 入出力関数を使ってファイルを読み書きする。
  3. ファイルをクローズする。

「ファイルをオープンする」とは、アクセスするファイルを指定して、それと一対一に対応するストリームを生成することです。入出力関数は、そのストリームを経由してファイルにアクセスします。SML/NJ の場合、ファイルをオープンするには関数 openIn と openOut を使います。オープンしたファイルは必ずクローズしてください。この操作を行う関数が closeIn と closeOut です。

val openIn   : string -> instream
val openOut  : string -> outstream
val closeIn  : instream -> unit
val closeOut : outstream -> unit

ファイル名は文字列で指定し、ファイル名のパス区切り記号にはスラッシュ ( / ) を使います。\ は文字列のエスケープコードに割り当てられているため、そのままではパス区切り記号に使うことはできません。ご注意ください。ファイルのオープンやクローズに失敗した場合は例外 Io が送出されます。

●input1 と output1

主な入出力関数を次に示します。

読み込み
val TextIO.input1    : instream -> char option 
val TextIO.inputLine : instream -> string option
書き込み
val TextIO.output1 : outstream * char -> unit
val TextIO.output  : outstream * string -> unit

関数 input1 は入力ストリームから 1 文字 (1 byte) 読み込みます。関数 inputLine はファイルから 1 行読み込みます。改行文字は削除されません。関数 output1 は出力ストリームに 1 文字 (1 byte) 書き込みます。関数 output は出力ストリームに 1 行書き込みます。

入力ストリームの場合、ファイルに格納されているデータには限りがあるので、ストリームからデータを取り出していくと、いつかはデータがなくなります。この状態を「ファイルの終了 (end of file : EOF)」 といいます。ファイルが終了したとき、input1 と inputLine は NONE を返します。また、ファイルの終了は次の関数でチェックすることができます。

val TextIO.endOfStream : instream -> bool

ファイルが EOF の場合、endOfStream は true を返します。そうでなければ false を返します。

簡単な例題として、ファイルの内容を画面へ出力する関数 cat を作ってみましょう。input1 と output1 を使うと、プログラムは次のようになります。

リスト : ファイルの表示 (1)

fun cat1 filename =
  let
    open TextIO
    val a = openIn( filename )
    fun cat_sub NONE = ()
    |   cat_sub(SOME c) = (output1(stdOut, c); cat_sub(input1 a))  
  in
    cat_sub(input1 a);
    closeIn a 
  end

関数 cat1 の引数 filename はファイル名を表す文字列です。最初にストラクチャ TextIO をオープンします。open は宣言なので、let と in の間に書くことができます。有効範囲は局所変数の場合と同じです。次に、openIn でファイルをオープンします。

ファイルの表示は関数 cat_sub で行います。cat_sub の引数は input1 a の返り値 (char option) です。NONE の場合はファイルが終了したのでユニットを返します。データがある場合は、パターンマッチングで文字 c を取り出し、output1 で c を標準出力 (stdOut) へ出力します。そして、input1 a で 1 文字読み込んで cat_sub を再帰呼び出しします。最後に、closeIn でファイルを閉じます。

cat1 は再帰呼び出しを使いましたが、繰り返しでも簡単にプログラムできます。次のリストを見てください。

リスト : ファイルの表示 (2)

fun cat2 filename =
  let
    open TextIO
    val a = openIn filename
    val c = ref (NONE: char option)
  in
    while (c := input1 a; isSome (!c)) do output1(stdOut, valOf(!c));
    closeIn a
  end

まず、option 型を格納する ref 変数 c を用意します。NONE は多相的なデータなので、型を char option に指定します。while ループの条件部は複文を使っています。最初に、ref 変数 c に input1 の返り値をセットし、次に関数 isSome でデータがあるかチェックします。これが複文の返り値になるので、isSome が false を返すと while ループが終了します。データがある間は output1 でデータを stdOut へ出力します。

●inputLine と output

次は、inputLine と output を使ってみましょう。プログラムは次のようになります。

リスト : ファイルの表示 (3)

fun cat3 filename =
  let
    open TextIO
    val a = openIn filename
    val b = ref (NONE: string option)
  in
    while (b := inputLine a; isSome (!b)) do output(stdOut, valOf(!b));
    closeIn a
  end

fun cat4 filename =
  let
    open TextIO
    val a = openIn filename
  in
    while (not(endOfStream a)) do output(stdOut, valOf(inputLine a));  
    closeIn a
  end

関数 cat3 は cat2 を行単位の入出力に改造しただけです。string option を格納する ref 変数 b を用意します。while ループの条件部で、inputLine の返り値を b にセットし、isSome でデータがあるかチェックします。NONE であれば while ループを終了します。そうでなければ、データを output で stdOut へ出力します。

関数 cat4 は endOfStream でファイルの終了をチェックしています。endOfStream はファイルが終了すると true を返すので、while ループの条件部では述語 not で結果を反転していることに注意してください。あとは inputLine で読み込んだデータを output で stdOut へ出力するだけです。

●ファイルの書き込み

データをファイルに書き込むには、ファイルを openOut でオープンします。このとき、注意事項が一つあります。既に同じ名前のファイルが存在している場合は、そのファイルの長さを 0 に切り詰めてからデータを書き込みます。既存のファイルは内容が破壊されることに注意してください。

それでは簡単な例題として、string list の要素を 1 行ずつファイルに書き込む関数 output_stringList を作ってみましょう。次のリストを見てください。

リスト : ファイルの書き込み

fun output_stringList(data, filename) =
  let
    open TextIO
    val a = openOut filename
  in
    app (fn x => output(a, x ^ "\n")) data;  
    closeOut a
  end

最初に openOut でファイルをオープンします。あとは、高階関数 app を使って data から要素を一つずつ取り出し、改行文字を付加してから output で出力するだけです。

このほかにも、SML/NJ にはいろいろな入出力関数が用意されています。詳しい説明は SML/NJ ライブラリのマニュアル The Standard ML Basis Library を参照してください。


初版 2005 年 6 月 11 日
改訂 2020 年 8 月 16 日