Effective ATS: ファイルコピー

ファイルコピーは概念的には単純ですが、にもかかわらず ATS で実装するのは実際興味深いかもしれません。

最初の試み

1つのファイルの内容物を別のファイルにコピーするために、関連するファイル群を参照する手段が必要になります。 Linux では、ファイルディスクリプタの概念がこの目的のために役立ちます。 私達はファイルディスクリプタが整数で表現されることを知っていますが、次のような宣言でそれを抽象化するのは適切でしょう:
abst@ype fildes = int
抽象型に衝突を引き起こしそうにない名前をつけるのはしばしば良い習慣です。 そしてその名前に短かい別名を導入するのです。 例えば、次の宣言はそのような習慣を示しています:
abst@ype fildes_t0ype = int
stadef fildes: t@ype = fildes_t0ype
私の命名規約では、種 t@ype の型、すなわちサイズが不明な型を表わすのに特殊な識別子 [t0ype] を使います。 stadef 宣言は次のようにも置き換え可能であることに注意してください:
typedef fildes = fildes_t0ype
さて、ファイルコピー関数を [fcopy1] と名付けて、次のインターフェイスを与えましょう:
fun fcopy1 (src: fildes, dst: fildes): void
[fcopy1] をどのように実装すべきでしょうか? 差し当たり、幾分抽象的な作法でこの問に答えることにしましょう。

[src] から文字を読み出し、[dst] へ文字を書き込めるべきであることは明確です。 そこで、次の2つの関数が使えると仮定しましょう:

fun readch (src: fildes): char
fun writech (src: fildes, c: char): void
またもう一つあります: 与えられたファイルから全ての文字を読み終わったかどうか分からなければなりません。 簡単な方法は、[readch] がファイルの最後に達したことを示す特別な値を返すようにすることです。 この目的のために、[readch] のインターフェイスを次のように修正します:
fun readch (src: fildes): int
非負の整数は有効な文字を、負の整数 (例えば -1) は ([src] の最後に達したことを示す) 特別な値とします。 これで [fcopy1] を次のようにたやすく実装することができます:
implement
fcopy1 (src, dst) = let
  val c = readch (src)
in
//
if c >= 0 then
  (writech (dst, c); fcopy1 (src, dst))
// end of [if]
//
end (* end of [fcopy1] *)
ここで当然次のような疑問がわくでしょう: 関数 [readch] と [writech] はどうすれば実装できるでしょうか? それらはシステムコール [read] と [write] に基づいて実装できます。 [fcopy1] に基づいたファイルコピーの実行可能な実装は fcopy1.dats から入手できます。 このとき [readch] と [writech] はC言語で直接実装されています。

もちろん、上記のファイルコピー実装には多くの批判があるでしょう。 例えば、エラー処理のようなものを全くサポートしないため、ほとんど役に立ちません。 これらの課題については次の試みで解決しようと思います。 けれども、この実装において建設的だったのは関数 [readch] と [writech] を導入したことです。 これらの関数はシステムコール [read] と [write] を直接使うことを防ぐレイヤーになっています。 このプログラミングスタイルを私は何度も繰り返し強く提言してきました。 私にとって非常に残念なことですが、たとえシステムプログラミングに関する著名な本 (例: APUE ) がこの有用なプログラミングスタイルを推奨していなかったとしてもです!

2番目の試み

[fcopy1] は [read] と [write] をそれぞれいちいち1文字ずつ読み書きするファイルコピーであるために、全く役に立たないことは明確です。 もし一度に複数文字読み出すのあれば、それらを保管するためのバッファ (つまりなんらかのメモリ) が必要になります。 そのようなバッファを静的に確保する場合のみを使うことはとても簡単なので、動的に確保されたバッファも扱えるような解決策にに集中することにします。 はじめにバッファを表わす抽象型を導入しましょう:
absvtype buffer_vtype = ptr
vtypedef buffer = buffer_vtype
[buffer_vtype] が viewtype、つまり線形型として導入されています。 そして次はそれぞれバッファを生成/削除するための関数です:
fun buffer_create (m: size_t): buffer
fun buffer_destroy (buf: buffer): void
また、バッファがデータを含むか否か検査する必要があります。 この目的のために次の関数を導入しましょう:
fun buffer_isnot_empty (buf: !buffer): bool
さらに、[readbuf] と [writebuf] をそれぞれ複数文字を読み書きする関数として作り、それらに次の型を割り当てましょう:
fun readbuf (src: fildes, buf: !buffer): void
fun writebuf (dst: fildes, buf: !buffer): void
[fcopy2] には [fcopy1] と同じインターフェイスを与えることにしましょう。 次のコードは [readbuf] と [writebuf] を用いた [fcopy2] の率直な実装です:
implement
fcopy2 (src, dst) = let
//
fun loop
(
  src: fildes, dst: fildes, buf: !buffer
) : void = let
  val () = readbuf (src, buf)
  val isnot = buffer_isnot_empty (buf)
in
  if isnot then let
    val () = writebuf (dst, buf) in loop (src, dst, buf)
  end else () // end of [if]
//
end // end of [loop]
//
val buf =
  buffer_create (i2sz(BUFSZ))
val () = loop (src, dst, buf)
val () = buffer_destroy (buf)
//
in
  // nothing
end (* end of [fcopy2] *)
[BUFSZ] はコンパイル時定数の整数、[i2sz] は型 [int] の整数から [size_t] の値へのキャスト関数であることに注意してください。 [fcopy2] を用いたファイルコピーの実行可能な実装は fcopy2.dats から入手できます。 このとき [readbuf] と [writebuf] はC言語で直接実装されています。

3番目の試み

[fcopy1] の非能率は [fcopy2] の実装で解決されましたが、エラー処理がありません。 この試みでは、ファイルコピーに別の実装を与えることで、エラー処理の問題を解決します。

[read] もしくは [write] の呼び出しは様々な要因で失敗する可能性があることは明白です。 もしそのような失敗が起きたら、おそらくファイルコピーを停止してエラー報告をすべきでしょう。 次のような別のファイルコピー関数 [fcopy3] を導入してみましょう:

fun fcopy3 (src: fildes, dst: fildes, nerr: &int): void
[fcopy3] の3番目の引数 [nerr] は参照渡し (call-by-reference) の整数です。 別の言い方をすると、[fcopy3] の 3番目の引数として渡されるのは、左辺値のアドレスです。 もしファイルコピーの最中に [read] もしくは [write] に起因するエラーが発生したら、[nerr] に保管されている整数値を増やすべきです。 これを実現するために、[readbuf] と [writebuf] の方を次のように変更します:
fun readbuf (src: fildes, buf: !buffer, nerr: &int): void
fun writebuf (dst: fildes, buf: !buffer, nerr: &int): void
関数 [readbuf] が [read] を呼び出すと; もしこの呼び出しがエラーを報告したら、[readbuf] はその3番目の引数に保管されている整数値を増やします。 関数 [writebuf] も [write] について同じ動作をします。 次のコードは [fcopy2] と同じように [fcopy3] を実装しています:
implement
fcopy3 (src, dst, nerr) = let
//
fun loop
(
  src: fildes, dst: fildes, buf: !buffer, nerr: &int
) : void = let
  val nerr0 = nerr
  val () = readbuf (src, buf, nerr)
  val isnot = buffer_isnot_empty (buf)
in
  if isnot then let
    val () = writebuf (dst, buf, nerr)
  in
    if nerr = nerr0 then loop (src, dst, buf, nerr) else ((*error*))
  end else () // end of [if]
//
end // end of [loop]
//
val buf = buffer_create (i2sz(BUFSZ))
val ((*void*)) = loop (src, dst, buf, nerr)
val ((*void*)) = buffer_destroy (buf)
//
in
  // nothing
end (* end of [fcopy3] *)
[read] もしくは [write] に起因するエラーが報告されると loop 関数が終了することに注意してください。 [fcopy3] を用いたファイルコピーの実行可能な実装は fcopy3.dats から入手できます。 このとき [readbuf] と [writebuf] はC言語で直接実装されています。
この記事は Hongwei Xi によって書かれ、 Japan ATS User Group によって翻訳されています。