1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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))))

このようになります。で、やはり最初の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 )

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?