Scheme でのファイルの読み書き (2) ファイルに追加書き込みする




Scheme でファイルに追加書き込みをやるにはどうすればいいのか謎だったので自分で書いてみました。


## Scheme 処理系によって append オプションが区々

「scheme append to file」でググってみると Gimp や Racket、Guile でファイルに追加書き込みする仕方が割と違っているので、共通の関数で書いてみようと思います。


## まずはコピー

紫藤のページ/もうひとつの Scheme 入門　の「9. 入出力」（ https://www.shido.info/lisp/scheme9.html ）にファイルのコピーのためのコードが掲載されています。（「練習問題の解答」の「練習問題 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)))))))
```

(call-)with-input/output-file 関数があるのに何故 open-input/output-file 関数があるのか不思議でしたがなるほど、こういう場合なら open-input/output-file 関数の方が直感的だと思います。

1. コピー元の読み込みポートをinput、コピー先の書き込みポートをoutputで取得し束縛
2. さらにコピー元の読み込みポートをread(-**)関数で文字にしたものを let ループの変数に束縛し
3. それが eof-object? で偽の間、読み込んだ文字をコピー先に書き出し続け
4. 最後にたどり着くと（eof-object? が真を返すと）、両ファイルのポートを閉じる

と、分かりやすいのではないでしょうか。
これを call-with-input/output-file で書くと

```
(define (copy-file file1 file2)
  (call-with-output-file file2
    (lambda (o-p)
      (call-with-input-file file1
	(lambda (i-p)
	  (let ruupu ([moji (read-char i-p)])
	    (unless
	     (eof-object? moji)
	     (display moji o-p)
	     (ruupu (read-char i-p)))))))))
```

でしょうか。call-with-input/output-file のネストになっていて、open-input/output-file のコードより分かりにくいかもしれません。



## ファイルを二つ使っての append

my-copy-file のコードを流用してコピー元のファイルを読み込み、最後に追加したい文字列を追加したファイルを作ることを考えます。
上記の４つの手順の4番め

4. 最後にたどり着くと（eof-object? が真を返すと）、両ファイルのポートを閉じる

を

4. 最後にたどり着くと（eof-object? が真を返すと）、追加したい文字列をコピー先のファイルに書き込み、両ファイルのポートを閉じる

にすると出来るはずです。

```
(define (append-file file1 file2 str)	; 追加したい文字列を第3引数（str）として追加
  (let ([i-p (open-input-file file1)]
		[o-p (open-output-file file2)])
    (let roop ((moji (read-line i-p)))
      (if (eof-object? moji)
	  (begin
	    (close-input-port i-p)
	    (display str o-p)				; これを追加
	    (close-output-port o-p))
	  (begin
	    (display
	     (string-append moji "\n")
	     o-p)
	    (roop (read-line i-p)))))))
```

文字列で append したいので read-char ではなく read-line を使っています。そのため１行書き込む度に改行文字を strring-append で接着させています。
これで file1 の内容に str が追加された内容が file2 に書き込めるようにできました。
しかしやはり元のファイルに追加できるようにしたいので、

1. file1 の内容に str が追加された内容を file2 に書き込み
2. それを file1 にコピーし
3. file2 を消去する

とやってみます。

```
(define (append-file file str)

  (use file.util)

  (let ([i-p (open-input-file file)]
	[o-p (open-output-file "temp-file")])
    (let roop ((moji (read-line i-p)))
      (if (eof-object? moji)
	  (begin
	    (close-input-port i-p)
	    (display str o-p)
	    (close-output-port o-p))
	  (begin
	    (display
	     (string-append moji "\n")
	     o-p)
	    (roop (read-line i-p))))))

  (let ([i-p (open-input-file "temp-file")]
	[o-p (open-output-file file)])
    (let roop ((moji (read-line i-p)))
      (if (eof-object? moji)
	  (begin
	    (close-input-port i-p)
	    (close-output-port o-p))
	  (begin
	    (display
	     (string-append moji "\n")
	     o-p)
	    (roop (read-line i-p))))))

  (remove-file "temp-file"))
```

これで一応、上の3手順を実行し、ファイルに文字列を append 出来るようになったわけですが……似たような手続き（違いは一行だけ）を繰り返しているのが Schemer には屈辱。どうにか簡略化したい。しかも temp-file を消去するために (use file.util) を使っていて、処理系に拠らない append を書くという主旨に反するような……（SRFI 170 には delete-file という手続きがあるようなのですが、 (use srfi-170) には「何それ？」と言われました）



## なのでコピー元の内容をバッファに一旦貯めて、自身に書き出す


最初に紹介したウェブページ（https://www.shido.info/lisp/scheme9.html）にこれに似た方針で書かれた read-file という関数が書かれているので、それを元に自身をコピーして自身に書き出す関数を考えます。

```
(define (self-copy file)
  (let ([i-p (open-input-file file)])
    (let roop ([moji (read-char i-p)]
			　　[mojiretsu '()])
      (if (eof-object? moji)
	  (begin
	    (let　((o-p (open-output-file file)))
	      (display (list->string　(reverse mojiretsu)) o-p)
	      (close-output-port o-p))
	    (close-input-port i-p))
	  (roop (read-char i-p)
		(cons moji mojiretsu))))))
```

1. ファイルの文字を一文字ずつ moji として読み
2. それを一文字ずつ mojiretsu というリストに入れていき
3. 逆向きに入っているそのリストを reverse で元の順に戻し
4. list-string で文字列に戻してファイルに書き込む

という関数です。
これで自身をコピーする手続きが書けたので、最後の手順をちょっと変えれば append できるはずです。

```
(define (append-file file str)
  (let ([i-p (open-input-file file)])
    (let roop ([moji (read-char i-p)]
	       [mojiretsu '()])
      (if (eof-object? moji)
	  (begin
	    (let ((o-p (open-output-file file)))
	      (display
	       [list->string (reverse (append (reverse (string->list str)) mojiretsu))]
		   o-p)
	      (close-output-port o-p))
	      (close-input-port i-p))
	    (roop (read-char i-p)
		  (cons moji mojiretsu))))))
```

4 を

追加したい文字列を str として渡し、それを string->list でリストにし、reverse して mojiretsu （元の内容がすべて逆向きに入っているリスト）に append し、さらに reverse して list->string で文字列に戻し自身に書き込んで終わり

に変えました。
これでファイルに文字列を追加できるようになりました！

が、何故か改行されて追加されたり改行されず追加されたりするんですよね……何でだろ？

## 参考

紫藤のページ/もうひとつの Scheme 入門　の「9. 入出力」（https://www.shido.info/lisp/scheme9.html）