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で閉じなければならないようです。何故かはよく分かりません。分かりませんが、やっぱ開きっ放しは良くないんじゃないですかね? バグを引き起こしそうですし。
その他は
- ファイルポートをread-charで読んで得た文字をmojiに入れ
- それがファイルの終わりならば(eof-object? で#tを返すならば)、close-input-portでポートを閉じて終わり
- そうでないならばmoji(に入っている一文字)をdisplayで表示し
- 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))))
このようになります。で、やはり最初の1行だけが表示されるので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 )