Scheme でのファイルの読み書き




Schemeでのファイルの読み書きの方法がとっつきにくかった（当社比）ので書いておきます。
環境はUbuntuにインストールしたGauche(0.9.9)、EmacsでREPLしてますが端末でも同じはず。


## ポート

とっつきにくかった理由の一つが「ポート」と言われる概念です。
Schemeではファイルに接する（アクセスする）ため、ファイルのある箇所（最初はファイルの先頭）をポートとして扱います。そしてやはりScheme、その「ポート」もデータとして操作します。



## 取り敢えずポートを使ってファイルを表示させるまで

Scheme処理系を走らせているディレクトリ（端末ならばプロンプトの階層。普通に端末を開くとホームだと思う）に"test"というテキストファイルがあるとします。内容は

```
aaa
bbb
ccc
```

とでも適当に書いときますね。
その"test"ファイルを読むためのポートは

```
(open-input-file "test")
```

で取得できます。読んで表示させるのにinputです。outputではありません。何でだ。

しかしこれを実行したところでこのファイルの先頭のポートが

```
#<oport a ホニャララ>
```

と返ってくるだけ、ファイルの内容は表示されません。しかも実行する度に同じファイルなのに違うポートが返ってきます。何でだ。
しょうがないのでそのポートをletでf-pという変数に束縛します。（file-portの略くらいに思いねぃ）

```
(let ((f-p (open-input-file "test"))))
```

当然これだけではポートが返ってくるだけです。なのでこのf-pを使ってファイルの内容を表示させます。
表示させるんでdisplayです。まんまです。

```
(let ((f-p (open-input-file "test")))
	(display f-p))
```

はい、やはりポートの値が返ってくるだけでファイルの内容は表示されません。
ファイルの内容を表示させるにはこのポートをread（-ホニャララ）関数で読む必要があります。

```
(let ((f-p (open-input-file "test")))
  (display (read f-p)))
```

はい、ようやく最初の行が

```
aaa#<undef>
```

と表示されました。最初の行だけです。read関数だとファイルの先頭のポートから改行までしか読んでくれません。read-char関数に至っては一文字だけです。
なのでこの一連をぐるぐる回して全表示に挑みます。Schemeなので再帰です。

```
(let ((f-p (open-input-file "test")))
  (let roop ((moji (read f-p)))
    (if (eof-object? moji)
		(close-input-port f-p)
		(begin
	  		(display moji)
	  		(roop (read f-p))))))

aaabbbccc#<undef>
```

改行されてませんね。SchemeのreadはS式を読んでくれるだけ（「改行文字？空白？見えねーな」ということだと思います）なので代わりにread-charを使います。

```
(let ((f-p (open-input-file "test")))
  (let roop ((moji (read-char f-p)))
    (if (eof-object? moji)
		(close-input-port f-p)
		(begin
	  		(display moji)
	  		(roop (read-char f-p))))))

aaa
bbb
ccc
#<undef>
```

はい、全文表示されました。ドヤァ！
あ、調子こいてすみません。「(close-input-port f-p)は何だよ？」ですよね。open-input-fileでファイルのポートを開いたらclose-input-portで閉じなければならないようです。何故かはよく分かりません。分かりませんが、やっぱ開きっ放しは良くないんじゃないですかね？　バグを引き起こしそうですし。
その他は
1. ファイルポートをread-charで読んで得た文字をmojiに入れ
2. それがファイルの終わりならば（eof-object? で#tを返すならば）、close-input-portでポートを閉じて終わり
3. そうでないならばmoji（に入っている一文字）をdisplayで表示し
4. read-charで一文字分ポートを読みそれをroopの引数にmojiとして渡しつつ、read-charに読ませることでポートを一文字分進め、2 へ
です。

4 が分かりにくいかもしれませんが、read(-char・その他)でポートを読むと、その読んだポートは消費されて次の文字のポート（read-lineなら次の行のポート）へ移ります。




## ともかくポートを使ってファイルに書き込むまで

ならば次はファイルに書き込みです。ご想像のとおり書き込むためのポートは

```
(open-output-file "ファイル名")
```

で取得できます。書き込むのにoutputです。書き込むのに。
読む時と同様にletで束縛します。書き込むファイルは"sample"にでもしときます？

```
(let ([f-p (open-output-file "sample")])
```

で、読む時のreadに対して書き込むときはwriteで

```
(let ([f-p (open-output-file "sample")])
  (write "aaa" f-p))
```

とすれば"sample"ファイルに"aaa"が書き込まれて……ない。
open-input-fileの時には「何でポートを閉じなきゃいけないのだろう……」とかグチグチ言ってましたがopen-output-fileの時はclose-output-portでポートを閉じないと反映されません。

```
(let ([f-p (open-output-file "sample")])
  (write "aaa" f-p)
  (close-output-port f-p))
```

で、"sample"に"aaa"が……

```
"aaa"
```

二重クオートまで書き込まれとるやないか！
いや本当、read/writeはややややこしいので泣きのdisplayを使います。「ファイルに」表示させるdisplayということで、どうか一つ。

```
(let ([f-p (open-output-file "sample")])
  (display "aaa" f-p)
  (close-output-port f-p))
```

これで"sample"ファイルに

```
aaa
```

と書き込まれました。



## しかし一々ポートを閉じるのはやはり面倒い

なので自分で閉じなくても勝手に閉じてくれる関数がSchemeにはあります。それが

```
call-with-input-file
call-with-output-file
```

です。しかしopen-input/output-fileとそのまま入れ替えても使えません。

```
(let ((f-p (call-with-input-file "test")))
  (display (read f-p)))
```

としても「引数が違ぇーよ」（意訳）と返ってきます。call-with-input/output-fileの書式は、第1引数にファイル、第2引数にそのファイルのポートを唯一の引数に取る関数、です。大抵はラムダ式にするんじゃないでしょうか。

```
(let ((f-p (open-input-file "test")))
  (display (read f-p)))
```

は

```
(call-with-input-file
    "test"
  (lambda (f-p) (display (read f-p))))
```

このようになります。で、やはり最初の１行だけが表示されるのでopen-input-fileと同じようにletループで回します。

```
(call-with-input-file
    "test"
  (lambda (f-p)
    (let roop ((moji (read-char f-p)))
      (if (not (eof-object? moji))
	  (begin
	    (display moji)
	    (roop (read-char f-p)))))))
```

同じようにっつって違うじゃねーかとのお叱りもあるかと存じますが、だってしょうがないじゃん、勝手にポートを閉じてくれるから eof-object? が真になったときにやることがないんだからさー。（なので if not 〜 ではなく unless を使うのが賢いやり方っぽいです）



書き込むときは同じく

```
(let ([f-p (open-output-file "sample")])
  (display "aaa" f-p)
  (close-output-port f-p))
```

を

```
(call-with-output-file
    "sample"
  (lambda (f-p)
    (display "aaa" f-p)))
```

に書き換えるだけです。こっちはポートを閉じてくれるのがありがたい。



## でもファイルに書き込む度に上書きされるのは困る

例えば上記の式を

```
(call-with-output-file
    "sample"
  (lambda (f-p)
    (display "bbb" f-p)))
```

に書き換えて評価すると"sample"は

```
aaabbb
```

にならず単に

```
bbb
```

に書き換えられてしまいます。なので現代Schemeの処理系にはだいたい append オプションがあるようです。
Gauche だと

```
(open-output-file "ファイル" :if-exists :append)
```

で追加書き込み用のポートが取得できます。あるいは

```
(call-with-output-file
    "ファイル"
  (lambda (f-p) (display  "追加したい文字列" f-p))
	:if-exists :append)
```

でそのまま書き込めます。ただ改行されずにそのまま最後の行に追加されるので、改行したい場合は

"追加したい文字列" => "\n追加したい文字列"

に変えると改行されて追加されます。


## ファイルに書き込むのにコードを書いて文字列を埋めるのは面倒い

でしょ？　というわけで

```
(call-with-output-file
    "ファイル"
  (lambda (f-p)
    (display (read) f-p)))
```

とするとREPLだとそのまま入力待ちになって、打ち込んだ文字（列）がファイルに書き込まれます。上書きでなく追加の場合は上記のようにappendオプションを加えてください。
二重クオートで囲うが囲うまいが関係なく出力されますが、改行させたいときは囲って"\n"を先頭に加えてください。

取り敢えずの Scheme でファイルの読み書きでした。


この記事を書くにあたり、以下の書籍・ウェブページをすごく参考にしました。


## 参考にした書籍・ウェブページ

湯浅太一『Scheme入門』（ISBN:9784000077019）

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

M.Hiroi's Home Page / お気楽 Scheme プログラミング入門 / Scheme の入出力 [1] ( http://www.nct9.ne.jp/m_hiroi/func/abcscm07.html )