M.Hiroi's Home Page

Functional Programming

お気楽 Scheme プログラミング入門

[ PrevPage | Scheme | NextPage ]

Scheme の入出力 [1]

今回はファイルからデータを読み込む、またはデータをファイルに書き込むなど、Scheme でデータの入出力を行うの方法について説明します。Scheme の仕様書 (R5RS, R7RS-small) には必要最低限の入出力機能だけしか定義されていないので、Gauche に用意されている便利な機能もあわせて紹介することにしましょう。最初に、最も簡単で基本的な「標準入出力」について説明します。

●標準入出力とは?

「標準入出力」は難しい話ではありません。実は、いままでに何回も使っているのです。たとえば、display を使ってデータを画面に表示しましたね。これはデータを「標準出力 (standard output)」へ出力していたのです。また、データの入力には read や read-char を使いました。これは「標準入力 (standard input)」からデータを受け取っているのです。一般に、標準入力にはキーボードが割り当てられていて、標準出力には画面が割り当てられています。

近代的なプログラミング言語の場合、ファイルをアクセスするためには「ストリーム (stream)」というデータを用います。辞書を引いてみると stream は「流れ」や「小川」という意味です。プログラミング言語の場合は、ファイルとプログラムの間でやりとりされるデータの流れ、という意味で使われているようです。

Scheme の場合は、「ポート (port)」というデータ型を介してファイルにアクセスします。ストリームと同様のデータと考えてください。ポートはファイルと 1 対 1 に対応していて、ファイルからデータを入力する場合、ポートを介してデータが渡されます。逆に、ファイルへデータを出力するときも、ポートを介して行われます。

Scheme 処理系を起動すると自動的に用意されるポートがあります。それが標準入出力に対応するポートです。

上図に示すように、ポートには出力と入力の 2 方向があります。起動した直後の標準入力、標準出力、標準エラー出力に対応するポートは次の関数で求めることができます。

標準エラー出力の出力先は標準出力と同じく「画面」ですが、エラーメッセージを出力するときに使います。なぜ、標準エラー出力が必要なのでしょうか。それは、標準出力がファイルへ「リダイレクト」されている場合でも、エラーメッセージを画面へ表示するためです。ここで「リダイレクト」について簡単に説明しましょう。

シェルには「リダイレクト機能」といって、標準入力や標準出力をほかのファイルへ切り替える機能を持っています。たとえば、ls コマンドはファイルの一覧を画面へ表示しますが、次のように出力先をファイルへ切り替えることができます。

$ ls -al > test.txt

この場合 test.txt というファイルに結果が格納されます。このときの > が標準出力先を変更する命令です。シェルは標準出力を画面から > の後ろに続くファイルへ切り替えます。

逆に、標準入力を切り替える命令が < です。シェルは標準入力をキーボードから < の後ろに続くファイルに切り替えます。キーボードからデータを受け取るように作られたプログラムであれば、リダイレクトによってファイルからの入力に切り替えることができます。

もしも、標準出力がほかのファイルへリダイレクトされているならば、エラーメッセージを標準出力へ出力しても画面上には表示されないことになります。このような場合でも、標準エラー出力を使えば画面へメッセージを出力することができます。

それでは、標準入出力ポートの値を見てみましょう。

gosh[r7rs.user]> (current-input-port)
#<iport (standard input) 0x7fb31ea05f00>
gosh[r7rs.user]> (current-output-port)
#<oport (standard output) 0x7fb31ea05e40>
gosh[r7rs.user]> (current-error-port)
#<oport (standard error output) 0x7fb31ea05d80>

Gauche の場合、入力ポートは #<iport ポートの名前 16進整数> と表示され、出力ポートは #<oport ポートの名前 16進整数> と表示されます。

●出力

次は、簡単な出力について説明します。今までデータを画面へ出力するのに display を使ってきました。display は画面だけではなく、出力先のポートを指定することができます。指定を省略した場合はデフォルト出力ポートが使用されます。Scheme を起動すると、デフォルト出力ポートには標準出力が割り当てられます。出力関数でポートの指定を省略すると、このポートを介してデータの出力が行われます。

出力ポートの指定は次のように行います。

display data oport

(display "Scheme\n" (current-error-port)) => 標準エラー出力へ出力する

(display "Scheme\n" (current-input-port)) => 入力ストリームはエラー!


                   図 : 出力ポートの指定

出力するデータの後ろに出力ポートを指定します。入力ポートを指定するとエラーになります。データを出力する関数は display のほかに write, write-char, write-string があります。

display は人間にとって読みやすい形式でデータを出力しますが、write は可能な限り read で読み込める形式でデータを出力します。write-char は文字型データ char を出力します。write-string は引数の文字列 string の start 番目から end - 1 番目の文字を出力します。

簡単な例を示しましょう。

gosh[r7rs.user]> (display "abcd\n")
abcd
#<undef>
gosh[r7rs.user]> (write "abcd\n")
"abcd\n"#<undef>
gosh[r7rs.user]> (display '(a "a" #\a))
(a a a)#<undef>
gosh[r7rs.user]> (write '(a "a" #\a))
(a "a" #\a)#<undef>
gosh[r7rs.user]> (write-char #\a)
a#<undef>
gosh[r7rs.user]> (write-string "acbdefg")
acbdefg#<undef>
gosh[r7rs.user]> (write-string "acbdefg" (current-output-port) 2)
bdefg#<undef>
gosh[r7rs.user]> (write-string "acbdefg" (current-output-port) 2 5)
bde#<undef>

display は文字列や文字を表示するときダブルクオートや #\ を表示しませんが、write は文字列を表示するときはダブルクオートで囲み、文字は #\ を付けて表示します。write-char と write-string は文字コードをそのまま出力します。

●format

ここで R5RS, R7RS-small にはありませんが、Gauche で用意されている便利な関数を紹介しましょう。format はデータを出力する関数ですが、単純に出力するのではなく、表示に関していろいろな指定を行うことができます。ですが、その分だけ使い方が少し複雑になります。

もともと format は Common Lisp の関数ですが、そのサブセットとして (または独自の拡張を行って) 定義している Scheme 処理系が多いようです。

format の第 1 引数は出力ポートを指定します。oport に #t が指定された場合はデフォルト出力ポートへ出力されます。#f が指定された場合は、ポートへ出力せずに変換結果を文字列にして返します。出力ポートが省略された場合は #f を指定した場合と同じ動作になります。

第 2 引数は書式文字列で、出力に関する指定を行います。これには文字列型データを使います。format は文字列をそのまま出力するのですが、文字列の途中にチルダ ~ が表れると、その後ろの文字を変換指示子として理解し、引数のデータをその指示に従って表示します。簡単な例を示しましょう。

gosh[r7rs.user]> (import (gauche base))
gosh[r7rs.user]> (format #t "~D ~B ~O ~X~%" 256 256 256 256)
256 100000000 400 100
#<undef>

R7RS モードで Gauche の機能を使用する場合はライブラリ (gauche base) を import してください。#<undef> は format の返り値です。書式文字列の中には、変換指示子をいくつ書いてもかまいません。チルダの前までは、そのまま文字を表示します。

チルダ ~ の次の文字 D, B, O, X が変換指示子です。これらの指示子は整数値を表示する働きをします。上の例が示すように、D は 10 進数、B は 2 進数、O は 8 進数、X は 16 進数で表示します。Gauche の場合、変換指示子は英小文字で書いてもかまいません。

変換指示子の数と引数として与えるデータの数が合わないとエラーになるので注意してください。また、~% は改行を表し、チルダを出力したい場合は ~~ と続けて書きます。

それから、チルダ ~ と変換指示子の間に前置パラメータやコロン ( : ) 修飾子、アットマーク ( @ ) 修飾子を指定することができます。簡単な例を示しましょう。

gosh[r7rs.user]> (format #t "[~D]~%" 10)
[10]
#<undef>
gosh[r7rs.user]> (format #t "[~4D]~%" 10)
[  10]
#<undef>
gosh[r7rs.user]> (format #t "[~4D]~%" 10000)
[10000]
#<undef>

整数値を表示する変換指示子は、前置パラメータでデータを表示するフィールド幅を指定することができます。最初の例がフィールド幅を指定しない場合で、次の例がフィールド幅を 4 に指定した場合です。10 ではフィールド幅に満たないので、右詰めに出力されます。もしも、フィールド幅に収まらない場合は、最後の例のように指定を無視して数値を出力します。

前置パラメータを複数指定する場合はカンマ ( , ) で区切ります。前置パラメータの意味は、変換指示子によって異なるので注意してください。簡単な例を示しましょう。

gosh[r7rs.user]> (format #t "[~4,'0D]~%" 10)
[0010]
#<undef>
gosh[r7rs.user]> (format #t "[~4,'aD]~%" 10)
[aa10]
#<undef>

整数値を表示する変換指示子の場合、第 1 番目の前置パラメータでフィールド幅を指定します。第 2 番目の前置パラメータに 'a を指定すると、左側の空いたフィールドに文字 a を詰め込みます。クオート ( ' ) は前置パラメータを文字として指定するときに用いられます。最初の例では文字 0 を詰め込み、次の例では文字 a を詰め込みます。

前置パラメータに文字 v を指定すると、引数の値が前置パラメータとして用いられます。簡単な例を示しましょう。

gosh[r7rs.user]> (format #t "~v,'0D~%" 4 10)
0010
#<undef>
gosh[r7rs.user]> (format #t "~v,'0D~%" 6 10)
000010
#<undef>
gosh[r7rs.user]> (format #t "~v,'0D~%" 8 10)
00000010
#<undef>

gosh[r7rs.user]> (format #t "~v,vd~%" 4 #\a 10)
aa10
#<undef>
gosh[r7rs.user]> (format #t "~v,vD~%" 6 #\b 10)
bbbb10
#<undef>
gosh[r7rs.user]> (format #t "~v,vD~%" 8 #\c 10)
cccccc10
#<undef>

前半の例では、フィールド幅の指定に引数の値が用いられます。そして、D 変換指示子により引数 10 が表示されます。後半の例では、詰め込む文字の指定に引数の値が用いられます。この場合、クオートを指定する必要はありません。ここで 'v と指定すると、詰め込む文字が v になってしまいます。

整数値を表示する変換指示子の場合、@ 修飾子を指定すると符号 (+/-) が必ず表示されます。: 修飾子を指定すると、3 桁ごとにカンマ ( , ) が表示されます。簡単な例を示します。

gosh[r7rs.user]> (format #t "~@D~%" 10)
+10
#<undef>
gosh[r7rs.user]> (format #t "~:D~%" 100000000)
100,000,000
#<undef>
gosh[r7rs.user]> (format #t "~,,' :D~%" 100000000)
100 000 000
#<undef>
gosh[r7rs.user]> (format #t "~,,,4:D~%" 100000000)
1,0000,0000
#<undef>

: 修飾子を使う場合、第 3 番目の前置パラメータで表示する区切り文字を指定することができます。また、区切る桁数は第 4 番目の前置パラメータで指定することができます。@ 修飾子と : 修飾子は、変換指示子によって意味が異なります。ご注意くださいませ。

S 式を表示する場合は A (a) または S (s) 変換指示子を使います。次の例を見てください。

gosh[r7rs.user]> (format #t "~A~%" "hello, world")
hello, world
#<undef>
gosh[r7rs.user]> (format #t "~S~%" "hello, world")
"hello, world"
#<undef>

A と S 変換指示子は、任意の S 式を出力できます。A は display と同じ形式で、S は write と同じ形式で出力します。A, S 変換指示子の場合でも、第 1 番目の前置パラメータでフィールド幅を指定することができます。

gosh[r7rs.user]> (format #t "[~20A]~%" "hello, world")
[hello, world        ]
#<undef>
gosh[r7rs.user]> (format t "[~20@A]~%" "hello, world")
[        hello, world]
#<undef>
gosh[r7rs.user]> (format #t "[~20,,,'*@A]~%" "hello, world")
[********hello, world]
#<undef>

A, S 変換指示子の場合、@ 修飾子を指定するとデータは右詰めに出力されます。詰め込む文字は第 4 番目の前置パラメータで指定します。

このほかにも、浮動小数点数を表示する指示子など、format にはたくさんの機能があります。詳細は Gauche のマニュアルをご覧ください。

●入力

次は、簡単な入力について説明します。今まで使ってきた read や read-char は入力ポートを指定することができます。指定を省略した場合はデフォルト入力ポートが使用されます。Scheme を起動すると、デフォルト入力ポートには標準入力が割り当てられます。入力関数でポートの指定を省略すると、このポートを介してデータの入力が行われます。

入力ポート (iport) の指定は次のように行います。

入力ポート iport がファイルの場合、格納されているデータには限りがあるので、入力ポートからデータを取り出していくと、いつかはデータがなくなります。この状態を「ファイルの終了」とか「ファイルの終端」 (end of file : EOF) といいます。ファイルが終了したとき、read と read-car は EOF を表すデータ型 (ファイル終端オブジェクト, EOF オブジェクト) を返します。簡単な例を示しましょう。

gosh[r7rs.user]> (read)
^D
#<eof>
gosh[r7rs.user]> (eof-object)
#<eof>
gosh[r7rs.user]> (eof-object? (eof-object))
#t

Unix 系 OS の場合、read で CTRL-D を入力すると EOF オブジェクトが返されます。Gauche の場合、EOF オブジェクトは #<eof> と表示されます。EOF オブジェクトは、述語 eof-object? でチェックすることができます。また、関数 (eof-object) で EOF オブジェクトを取得することができます。

ここで簡単な例題を見てみましょう。標準入力からデータを受け取って、それを標準出力へ出力するだけのプログラムです。標準入力をファイルにリダイレクトすれば、その内容を表示することができます。名前は cat としました。

リスト : 標準入力のデータをそのまま標準出力へ出力する (cat.scm)

(import (scheme base) (scheme read) (scheme write))

(define (cat)
  (let ((c (read-char)))
    (unless
     (eof-object? c)
     (display c)
     (cat))))

;;; 実行
(cat)

標準入力から read-char でデータを 1 文字読み込んで、その返り値を局所変数 c にセットします。次に、c が EOF オブジェクトか eof-object? でチェックします。そうでなければ、display で文字 c を出力して type を再帰呼び出しします。このプログラムは末尾再帰で書かれていますが、Scheme は末尾再帰最適化が行われるので、繰り返しで書かれたプログラムと同様に動作します。

標準入力をファイルへリダイレクトすると、ファイルの内容を表示することができます。

$ gosh cat.scm < cat.scm

・・・ cat.scm の内容が表示される ・・・

標準出力もほかのファイルへリダイレクトすれば、cat.scm をコピーすることもできます。

$ gosh cat.scm < cat.scm > test.scm

ここで display の出力ポートを (standard-error-port) に変更すると、標準出力へリダイレクトされていても、画面にデータが表示されます。

●read-line と read-string

R5RS にはありませんが、R7RS-small に用意されている関数を紹介しましょう。関数 read-line は入力ポートからデータを読み込み、改行文字までのデータを文字列にして返します。

改行文字は文字列に含まれないことに注意してください。もともと read-line は Common Lisp にある関数です。

簡単な例を示しましょう。read-line でテキストファイルの内容を表示します。

リスト : テキストファイルの表示

(import (scheme base) (scheme read) (scheme write))

(define (cat)
  (let ((buff (read-line)))
    (unless
     (eof-object? buff)
     (display buff)
     (newline)
     (cat))))

;;; 実行
(cat)

read-line はファイルの終端に達すると EOF オブジェクトを返します。ファイルを行単位で扱う場合、read-line はとても便利です。

行番号を付加することも簡単です。

リスト : テキストファイルの表示 (行番号付き)

(import (scheme base) (scheme read) (scheme write) (gauche base))

(define (cat n)
  (let ((buff (read-line)))
    (unless
     (eof-object? buff)
     (format #t "~6D: ~A~%" n buff)
     (cat (+ n 1)))))

;;; 実行
(cat 1)
$ gosh cat.scm < cat.scm
     1: (import (scheme base) (scheme read) (scheme write) (gauche base))
     2:
     3: (define (cat n)
     4:   (let ((buff (read-line)))
     5:     (unless
     6:      (eof-object? buff)
     7:      (format #t "~6D: ~A~%" n buff)
     8:      (cat (+ n 1)))))
     9:
    10: ;;; 実行
    11: (cat 1)

このように format を使えば行番号を付け加えることも簡単にできます。

関数 read-string は入力する文字数を指定します。

入力ポート iportから k 個の文字またはファイルの終端までに有る限りの文字を読み取り、それを文字列に格納して返します。read-string はファイルの終端に達すると EOF オブジェクトを返します。簡単な使用例を示します。

gosh[r7rs.user]> (read-string 4)
abcd
"abcd"
gosh[r7rs.user]> (read-string 4)
ab
c
"ab\nc"

●ファイル入出力

今度は、標準入出力を使わないでファイルにアクセスする方法を説明しましょう。ファイルをアクセスするためには、次の 3 つの操作が基本になります。

  1. アクセスするファイルをオープンする。
  2. 入出力関数を使ってファイルを読み書きする。
  3. ファイルをクローズする。

「ファイルをオープンする」とは、ファイルを名前 (ファイル名) で指定して、それと1 対 1 に対応するポートを作成することです。入出力関数は、そのポートを介してファイルにアクセスします。R7RS-small でファイルをオープンするにはライブラリ (scheme file) に用意されている関数を使います。

どちらの関数も引数 filename でファイル名を文字列で指定します。なお、Windows で Gauche を使う場合、パスの区切りは '\' ではなくて、次のように '/' を使うといいでしょう。

C:\usr\work\test.dat => "C:/usr/work/test.dat" 

ファイルからデータを読み込むときは open-inpuf-file でファイルをオープンします。open-input-file はファイルに対応する入力ポートを作成して返します。ファイルが存在しない、またはファイルがオープンできない場合はエラーになります。

ファイルにデータを書き込むときは open-output-file でファイルをオープンします。open-output-file はファイルに対応する出力ポートを作成して返します。ファイルがオープンできない場合はエラーになります。

ファイルが存在しない場合は新しいファイルを作成します。既存のファイルがある時の動作は、R7RS-small では規定されていません。Gauche では、そのファイルを 0 に切り詰めてからデータを書き込みます。

これらの動作は、一般的なプログラミング言語でファイルをオープンするときの動作と同じです。Gauche の場合、これとは異なる例外的な動作を指定することもできます。詳細は Gauche のマニュアルをお読みください。

通常は返り値のポートを変数に格納しておき、それを入出力関数に渡してファイルにアクセスします。そして、最後にファイルをクローズします。これは次の関数を使います。

close-input-port は入力ポート iport に対応するファイルをクローズします。close-output-port は出力ポート oport に対応するファイルをクローズします。close-port はポート port に対応するファイルをクローズします。オープンしたファイルは必ずクローズするようにしてください。

これらの関数を使うと、ファイルのアクセスは次のようなプログラムになるでしょう。

リスト : ファイルのアクセスの例 (その1)

(import (scheme base) (scheme read) (scheme write) (scheme file))

(define (cat1 filename)
  (let ((iport (open-input-file filename)))
    (let loop ((c (read-char iport)))
      (unless
       (eof-object? c)
       (display c)
       (loop (read-char iport))))
    (close-input-port iport)))

(cat1 "cat1.scm")

let でポートを格納する変数 iport を用意して、open-input-file でファイルをオープンします。ファイルのアクセスが終了したら、最後に close-input-port で iport に対応するファイルをクローズします。

●call-with-input-file と call-with-output-file

実をいうと、Scheme にはファイルのオープンとクローズを行ってくれる便利な関数がライブラリ (scheme file) に用意されています。

call-with-input-file は filename で指定されたファイルをオープンし、その入力ポートを関数 proc の引数に渡して評価します。proc が引数をひとつ取らない場合はエラーになります。proc の評価が終了すると、call-with-input-file は作成した入力ポートをクローズし、proc の返り値をそのまま返します。call-with-output--file はポートの種類が出力ポートになるだけで、あとは call-with-input-file と同じです。

Lisp / Scheme など関数型言語の場合、関数はほかのデータと同等に取り扱うことができます。つまり、関数を変数に代入したり、引数として渡すことができるのです。また、値として関数を返すこともできるので、関数を作る関数を定義することも簡単にできます。関数を引数に受け取る関数を「汎関数 (functional)」とか「高階関数 (higher order function)」と呼びます。高階関数については次回以降で詳しく説明します。

先ほどのプログラムを call-with-input-file で書き直すと、次のようになるでしょう。

リスト : ファイルのアクセスの例 (その2)

(import (scheme base) (scheme read) (scheme write) (scheme file))

(define (cat port)
  (let ((c (read-char port)))
    (unless
     (eof-object? c)
     (display c)
     (cat port))))

(define (cat2 filename)
  (call-with-input-file filename cat))

;;; 実行
(cat2 "cat2.scm")

このほかに、デフォルトポートの値を変更して、ファイル入出力を行う関数も用意されています。

with-inpur-from-file は filename で指定されたファイルをオープンし、その入力ポートをデフォルト入力ポートに設定します。それから、引数 thunk に渡された関数を評価します。thunk は引数なしで評価されるので、引数を受け取らない関数を渡すようにしてください。

thunk の評価が終了すると、with-input-form-file は作成した入力ポートをクローズし、デフォルト入力ポートを元の値に戻します。with-input-from-file は thunk の返り値をそのまま返します。with-output-to-file はポートの種類が出力ポートになるだけで、あとは with-input-form-file と同じです。

●コマンドラインからパラメータを受け取る方法

さて、ファイル操作を行うプログラムを作る場合、直接プログラムにファイル名を書き込むと、それをほかのファイルに使おうとしたときに、ファイル名を書き換えなければいけません。これは面倒なことですね。プログラムを起動するときに、コマンドラインからファイル名を指定できると便利です。

R7RS-small の場合、ライブラリ (scheme process-context) に用意されている関数 command-line を使うと、コマンドラインのパラメータを取得することができます。

command-line はコマンドラインを文字列のリストとして返します。簡単な使用例を示します。

リスト : パラメータの取得 (test.scm)

(import (scheme base) (scheme write) (scheme process-context))

(write (command-line))
(newline)

実行結果は次のようになります。

$ gosh test.scm abc def ghi jkl
("test.scm" "abc" "def" "ghi" "jkl")

引数に渡されるリストには、実行したファイル名が先頭に格納されることに注意してください。

簡単な例として、複数のファイル名を受け取るように cat.scm を改造してみましょう。次のリストを見てください。

リスト : ファイルの内容を表示する

(import (scheme base) (scheme write) (scheme read) 
        (scheme file) (scheme process-context))

(define (cat port)
  (let ((c (read-char port)))
    (unless
     (eof-object? c)
     (display c)
     (cat port))))

(define (cat3 files)
  (let loop ((files files))
    (when
     (pair? files)
     (call-with-input-file (car files) cat)
     (loop (cdr files)))))

;;; 実行
(cat3 (cdr (command-line)))

関数 command-line でパラメータを受け取ります。先頭の要素には実行ファイル名 cat.scm が格納されています。cdr でそれを取り除いて関数 cat3 に渡します。cat3 では名前付き let で繰り返しに入ります。あとは、リスト filess からファイル名を順番に取り出して call-with-input-file に渡すだけです。これで複数のファイルを続けて表示することができます。

●まとめ

今回はここまでです。簡単に復習しておきましょう。

  1. ファイルの入出力はポートを介して行う。
  2. 入出力関数はポートの指定がない場合、デフォルトのポート (標準入出力) が使用される。
  3. write は可能な限り read で読み込める形式でデータを出力する。
  4. format は書式を指定してデータを出力する。
  5. read, read-char など入力関数はファイルの終了時にファイル終端オブジェクトを返す。
  6. read-line はファイルから 1 行読み込んで、文字列型データとして返す。
  7. ファイルのオープンには open-input-file, open-output-file を使う。
  8. ファイルのクローズには close-port, close-input-port, close-output-port を使う。
  9. call-with-input-file と call-with-output-file は指定したファイルをオープンし、その後でファイルを自動的にクローズをする。
  10. コマンドラインからのパラメータを受け取るには関数 command-line を使う。

次回はもう少し実用的なプログラムを作ってみましょう。


初版 2007 年 12 月 30 日
改訂 2020 年 8 月 30 日

Copyright (C) 2007-2020 Makoto Hiroi
All rights reserved.

[ PrevPage | Scheme | NextPage ]