HOME もうひとつの Scheme 入門 書き込む

9. 入出力


1. 初めに

前回までで、関数定義について一通り説明したので、対話的な環境で関数を書いて、動かすことが できるようになったと思います。

今回は入出力について説明します。入出力ができればかなり実用的なプログラムを書くことができます。

2. ファイルからの入力

2.1. open-input-file, read-char, eof-object?

入力用にファイルを開くには (open-input-file filename) を使います。 この関数は入力用ポートを返します。 ポートから1文字読み込むには (read-char port) を使います。 read-char はファイルの終端に達すると eof-object を返すので、 eof-object? を使って、ファイルが終わったかどうか調べます。ファイルを閉じるときは (close-input-port port) を使います。 ファイルの内容を文字列として返す関数は以下のように書けます。

[code 1]

(define (read-file file-name)
  (let ((p (open-input-file file-name)))
    (let loop((ls1 '()) (c (read-char p)))
      (if (eof-object? c)
	  (begin
	    (close-input-port p)
	    (list->string (reverse ls1)))
	  (loop (cons c ls1) (read-char p))))))
たとえば、次のような内容のファイル (C:\doc\hello.txt) を作って試してみると [example 1] のような結果が得られます。 改行文字は '\r\n' で表されるので、少し見にくいのですが、ちゃんと読まれいているのがわかります。 出力用関数 display を使うと整形されます [example 2]。 [hello.txt]
Hello world!
Scheme is an elegant programming language.
[example 1]
> (current-directory "Z:/doc/scheme")
> (current-directory )
#<path:C:\doc\scheme\>
> (read-file "hello.txt")
"Hello world!\r\nScheme is an elegant programming language.\r\n"
[example 2]
> (display (read-file "hello.txt"))
Hello world!
Scheme is an elegant programming language.

2.2. call-with-input-file, with-input-from-file

call-with-input-file, with-input-from-file を使ってファイルを開くこともできます。エラー処理をしてくれるのでこちらの方が便利かもしれません。
(call-with-input-file filename procedure)
filename のファイルを入力用に開きます。 precedure は入力ポートを引数にとる関数です。 入力ポートが再び使われる可能性があると、precedure から制御が戻ってきたときにファイルが閉じられないので、 明示的にファイルを閉じた方が良いでしょう。 [code 1] の read-file を call-with-input-file を使って書くと [code 2] のようになります。

[code 2]

(define (read-file file-name)
  (call-with-input-file file-name
    (lambda (p)
      (let loop((ls1 '()) (c (read-char p)))
	(if (eof-object? c)
	    (begin
	      (close-input-port p)
	      (list->string (reverse ls1)))
	    (loop (cons c ls1) (read-char p)))))))
(with-input-from-file filename procedure)
filename を標準入力として開きます。procedure は引数なしの関数です。 procedure から制御が戻るとファイルは閉じられます。これが一番便利が良いでしょう。 [code 1] の read-filewith-input-from-file を使って書くと [code 3] のようになります。

[code 3]

(define (read-file file-name)
  (with-input-from-file file-name
    (lambda ()
      (let loop((ls1 '()) (c (read-char)))
	(if (eof-object? c)
	    (list->string (reverse ls1))
	    (loop (cons c ls1) (read-char)))))))

2.3. read

(read port) は port からScheme の式を読み込みます。 例えば、次のように括弧でくくられた文書を読むときは read は便利です。

[paren.txt]

'(Hello world!
Scheme is an elegant programming language.)

'(Lisp is a programming language ready to evolve.)
[code 4]
(define (s-read file-name)
  (with-input-from-file file-name
    (lambda ()
      (let loop ((ls1 '()) (s (read)))
	(if (eof-object? s)
	    (reverse ls1)
	    (loop (cons s ls1) (read)))))))
paren.txt を [code 4] の関数で読むと、次のようになります。
> (s-read "paren.txt")
((quote (hello world! scheme is an elegant programming language.))
(quote (lisp is a programming language ready to evolve.)))

練習問題 1

ファイルの内容を1行ずつのリストにして返す関数 read-lines を書いてください。 hello.txt に適用すると次のようになるようにして下さい。'\n' は #\Newline です。 改行文字は残すようにしてください。
(read-lines "hello.txt") ⇒ ("Hello world!\r\n" "Scheme is an elegant programming language.\r\n")

3. ファイルへの出力

3.1. 出力用ポート

入力用ポートを作るのと同様な関数が用意されています。
(open-output-file filename)
出力用にファイルを開きます。出力用ポートを返します。
(close-output-port port)
出力用 port を閉じます。
(call-with-output-file filename procedure)
filename を出力用に開いて procedure を行います。 procedure は ポートを引数に取る関数です。
(with-output-to-file filename procedure)
filename を標準出力として開き、procedure を行います。 procedure は引数なしの関数です。procedure から制御が戻るとファイルは閉じられます。

3.1. 出力用関数

以下の出力用関数があります。いずれの関数も、port を省略すると標準出力に出力されます。
(write obj port)
portobj を出力します。文字列は2重引用符で囲まれ、文字は #\ 形式で出力されます。
(display obj port)
portobj を出力します。文字列は2重引用符で囲まれず、文字はそのまま出力されます。
(newline port)
改行します。
(write-char char port)
charport に出力します。

練習問題 2

ファイルをコピーする関数 (my-copy-file) を書いてください。

練習問題 3

任意個の文字列の引数をとり、それらを標準出力に1行に1つずつ出力する関数 print-lines を書いてください。

4. 終わりに

Scheme の入出力は必要最低限しかないので、説明は簡単に済みました。 今回までの説明で普通のプログラムは Scheme でかけるようになったと思います。 次回は代入について説明します。

練習問題の解答

練習問題 1

まず、リストをセパレータで分割する group-list を作ります。 次に、ファイルから読み取った文字のリストを #\Newline で、分割し、 それぞれのリストを文字列に変換します。(; * の部分)
(define (group-list ls sep)
  (letrec ((iter (lambda (ls0 ls1)
		   (cond
		    ((null? ls0) (list ls1))
		    ((eqv? (car ls0) sep) 
                     (cons (cons sep ls1) (iter (cdr ls0) '())))
		    (else (iter (cdr ls0) (cons (car ls0) ls1)))))))
    (map reverse (iter ls '()))))


(define (read-lines file-name)
  (with-input-from-file file-name
    (lambda ()
      (let loop((ls1 '()) (c (read-char)))
	(if (eof-object? c)
	    (map list->string (group-list (reverse ls1) #\Newline))  ; *
	    (loop (cons c ls1) (read-char)))))))
実行例:
> (group-list '(1 4 0 3 7 2 0 9 5 0 0 1 2 3) 0)
((1 4 0) (3 7 2 0) (9 5 0) (0) (1 2 3))

> (read-lines "hello.txt")
("Hello world!\r\n" "Scheme is an elegant programming language.\r\n" "")

練習問題 2

(define (my-copy-file from to)
  (let ((pfr (open-input-file from))
	(pto (open-output-file to)))
    (let loop((c (read-char pfr)))
      (if (eof-object? c)
	  (begin
	    (close-input-port pfr)
	    (close-output-port pto))
	  (begin
	    (write-char c pto)
	    (loop (read-char pfr)))))))

練習問題 3

(define (print-lines . lines)
  (let loop((ls0 lines))
    (if (pair? ls0)
        (begin
         (display (car ls0))
         (newline)
         (loop (cdr ls0))))))