Let's start Scheme

(はじめよう Scheme 8)

I/O

Schemeに於けるI/Oはポートと呼ばれるオブジェクトを介して行われる。まずは簡単な例を見てみよう。以下はdata.txtというファイルから一文字ずつ読み取り標準出力に書き出すスクリプトである。
(import (scheme base) (scheme file))

(let ((input-port (open-input-file "data.txt")))
  (let loop ((ch (read-char input-port)))
    (if (eof-object? ch)
        (close-input-port input-port)
        (begin
          (write-char ch (current-output-port))
          (loop (read-char input-port))))))
open-input-fileでファイルポートを開き、read-charで一文字ずつ読み取る。そしてwrite-charで読み取った文字を現在の出力ポート(この場合は標準出力)に書き出す。eof-object?はポートから返ってきた値がEOFであるかどうかをチェックする。EOFであればポートを閉じて終了する。ポートを用いてデータを扱う際はこのような形になることが多いので覚えておくとよいだろう。

上記のスクリプトで使用した手続き
  • (open-input-file filename)
    テキスト入力ポートをfilenameファイル名で指定されたファイルで開く。
  • (read-char)
    (read-char input-port)
    入力ポートinput-portから一文字読み取る。input-portが省略された場合はcurrent-input-portから読み取る。入力ポートはテキストポートでなければならない。
  • (write-char char)
    (write-char char output-port)
    出力ポートoutput-portに文字charを書き出す。output-portが省略された場合はcurrent-output-portに書き出す。出力ポートはテキストポートでなければならない。
  • (eof-object? object)
    objectがEOFオブジェクトであれば#tを返す。そうでなければ#f
  • (close-input-port input-port)
    入力ポートinput-portを閉じる。閉じられたポートへの操作はエラーとなる。
ポートは大きくバイナリポートとテキストポートに分けられる。バイナリポートはその名の通りバイナリの読書きを行えるポートであり、テキストポートは文字の読書きになる。R7RS-smallではこの二つのポートは区別されなくてもよいとされており、処理系によってはバイナリとテキスト両方を扱うことができる(例:Gauche)。逆にこの二つをはっきりと分ける処理系もあるので(例:Sagittarius)、ポータブルなコードを書く際はどちらのポートを使用しているのかを頭に入れておいた方いい。特にcurrent-output-portはデフォルトではテキストポートを返すので、バイナリポートとして開いたファイルをデフォルトのcurrent-output-portが返すポートに書き出すのはエラーである。

ファイルをテキストポートとして開いたので、次はバイナリポートとして開いてみよう。上記に書いたように、current-output-portにそのままは書き出せないので、別のファイルにコピーする形にする。
(import (scheme base) (scheme file))

(let ((input-port (open-binary-input-file "in.dat"))
      (output-port (open-binary-output-file "out.dat")))
  (let loop ((u8 (read-u8 input-port)))
    (if (eof-object? u8)
        (begin
          (close-input-port input-port)
          (close-output-port output-port))
        (begin
          (write-u8 u8 output-port)
          (loop (read-u8 input-port))))))
-charの部分が-u8に変わったくらいで他はあまり変わりない。バイナリポートを扱う際に気をつけなければならないのはその値である。例えばread-u8はポートから1オクテット読み取る手続きで、返される値は0 <= n <= 255の範囲の正確な整数もしくはEOFオブジェクトである。write-u8の第一引数は0 <= n <= 255の範囲の正確な整数でなければならない。

上記のスクリプトで使用した手続き
  • (open-binary-input-file filename)
    バイナリ入力ポートをfilenameファイル名で指定されたファイルで開く。
  • (open-binary-output-file filename)
    バイナリ出力ポートをfilenameファイル名で指定されたファイルで開く。既にファイルが存在していた場合はエラーである。
  • (read-u8)
    (read-u8 input-port)
    入力ポートinput-portから一文字読み取る。input-portが省略された場合はcurrent-input-portから読み取る。入力ポートはバイナリポートでなければならない。
  • (write-u8 u8)
    (write-u8 u8 output-port)
    出力ポートoutput-portに文字charを書き出す。output-portが省略された場合はcurrent-output-portに書き出す。出力ポートはバイナリポートでなければならない。
  • (close-output-port output-port)
    出力ポートoutput-portを閉じる。閉じられたポートへの操作はエラーとなる。
read-u8write-u8でポートを省略した呼び出しはデフォルトのポートであればエラーになる。これらはwith-input-from-file等の手続きでデフォルトのポートを変更することを想定されている。これらのポートにバイナリポートを設定するにはparameterizeマクロを使う必要がある。parameterizeマクロは別の機会に紹介することにする。

設問8.1
上記のバイナリポートを例をテキストポートを扱うように変更せよ。出力ポートはopen-output-file手続きを用いて生成する。
  • (open-output-file filename)
    テキスト出力ポートをfilenameファイル名で指定されたファイルで開く。既にファイルが存在していた場合はエラーである。

文字列ポートとバイトベクタポート

ポートはその下にある実際の入出力を包んで共通のAPIを提供する仕組みといえる。そうするとファイル以外の入出力も扱えてもいいはずだ。R7RS-smallではファイルの入出力の他に、文字列とバイトベクタをポートとして扱う手続きが用意されている。文字列を入力ポートにして1行ずつ読んでみる。
(import (scheme base))

(define s "something very long.\nMaybe created by other procedure?\n")

(define input-port (open-input-string s))

(let loop ((line (read-line input-port)))
  (if (eof-object? line)
      (close-input-port input-port)
      (begin
        (write-string line (current-output-port))
        (newline (current-output-port))
 (loop (read-line input-port)))))
open-input-stringで入力文字列ポートを開き、read-lineで一行ずつ読み込んでいる。この例ではあまり見えないのだが、文字列ポートを使うことでいくつかのメリットを得ることができる。代表的なものを挙げてみよう。
  • input-portをファイルポートにしても同様に動く。
    手続きにポートを渡すようにすれば、実際の入力が何であるかということを気にしなくてもよくなる。
  • 文字列のアクセスは遅い場合がある
    処理系によっては文字列のn番目の文字にアクセスするのにO(n)かかるので、以下のように書くより高速である場合がある。
    (let loop ((i 0))
      (unless (= i (string-length s))
        (write-char (string-ref s i) (current-output-port))
        (loop (+ i 1))))
    
    また、このように書くと文字列限定の処理になるため汎用性が低い。 
入力用のポートがあるということは出力用のポートもある。open-output-stringがそれである。この手続きで開かれた出力用文字列ポートは内部に文字列バッファを持ち、get-output-stringでバッファに溜め込まれた文字列を取得することができる。

文字列ポートが文字に対するポートならばバイトベクタポートはバイナリに対するポートである。使い方は文字列ポートと同様、open-input-bytevectorでポートを開く。バイトベクタポートはバイナリポートなのでread-line相当の手続きは存在しない。read-u8もしくはread-bytevectorを使用する必要がある。出力用のポートはopen-output-bytevectorで開き、溜め込まれたバイトベクタはget-output-bytevectorで取得することができる。

上記スクリプトで使用した手続き
  • (open-input-string string)
    与えられた文字列stringをソースとする入力文字列ポートを開く。開かれたポートはテキストポートになる。stringが変更された際の挙動は未定義。
  • (open-input-bytevector bytevector)
    与えらてたバイトベクタbytevectorをソースとする入力バイトベクタポートを開く。開かれたポートはバイナリポートになる。bytevectorが変更された際の挙動は未定義。
  • (open-output-string)
    (open-output-bytevector)
    出力用文字列ポート、バイトベクタポートを開く。
  • (get-output-string port)
    手続き呼び出し時までに文字列ポートportに溜め込まれた文字列を取得する。ポートの位置、バッファは変更されない。
  • (get-output-bytevector port)
    手続き呼び出し時までにバイトベクタポートportに溜め込まれた文字列を取得する。ポートの位置、バッファは変更されない。
設問8.2
テキストポートを受け取り各行の偶数列にある文字のみを含んだ文字列を返すスクリプトを作成せよ。

No comments:

Post a Comment