M.Hiroi's Home Page

Functional Programming

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

[ PrevPage | Scheme | NextPage ]

Scheme の入出力 [2]

前回は Scheme の基本的な入出力処理について説明しました。ファイルはポートを介してアクセスすることがおわかりいただけたと思います。今回は簡単な応用プログラムに挑戦してみましょう。

●平均値と標準偏差

最初は、数値データを読み込んで、その平均値と標準偏差を求めるプログラムを作りましょう。平均値 \(M\)は次の式で求めることができます。

\( M = \dfrac{x_1 + x_2 + \cdots + x_N}{N} = \dfrac{1}{N} \displaystyle \sum_{i=1}^N x_i \)

統計学では、平均値からのばらつき具合を表すのに「分散 (variance)」という数値を使います。分散の定義を次に示します。

\( S^2 = \dfrac{(x_1 - M)^2 + (x_2 - M)^2 + \cdots + (x_N - M)^2}{N} = \dfrac{1}{N} \displaystyle \sum_{i=1}^{N} (x_i - M)^2 \)

標準偏差 \(S = \sqrt{S^2}\)

分散の定義からわかるように、平均値から離れたデータが多いほど、分散の値は大きくなります。逆に、平均値に近いデータが多くなると分散は小さな値になります。そして、分散の平方根が「標準偏差 (SD : standard deviation)」になります。標準偏差はデータのばらつき具合を表すのによく使われています。統計学に興味のある方は、拙作のページ Algorithms with Python 番外編 統計学の基礎知識 [1] をお読みください。

平均値と標準偏差を求めるプログラムは、データを読み込んでリストに格納しておくと簡単です。次のプログラムを見てください。

リスト : 平均値と標準偏差 (mean_sd.scm)

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

;;; データを読み込む
(define (read-data)
  (let loop ((ls '()))
    (let ((x (read)))
      (cond
       ((eof-object? x) ls)
       ((number? x)
        (loop (cons x ls)))
       (else
        (display "error data ")
        (display x)
        (display " \n")
        '())))))

;;; リストの合計値を求める
(define (sum-of-list ls)
  (let loop ((ls ls) (a 0.0))
    (if (null? ls)
        a
        (loop (cdr ls) (+ (car ls) a)))))

;;; 平均値と標準偏差を求める
(define (mean-sd ls)
  (let* ((n (length ls))
         (m (/ (sum-of-list ls) n)))
    (let loop ((s 0.0) (ls ls))
      (if (null? ls)
          (list m (sqrt (/ s n)))
          (loop (+ s (expt (- (car ls) m) 2)) (cdr ls))))))

;;; 実行
(display (mean-sd (read-data)))
(newline)

R7RS-small の場合、平方根を求める関数 sqrt はライブラリ (scheme inexact) に定義されています。三角関数 (sin, cos, tan) や対数 (log) などの数学関数も同じライブラリに定義されています。累乗は関数 expt で求めることができます。expt は (scheme base) に定義されています。

関数 read-data は標準入力ポートから read でデータを読み込み、それをリストに格納して返します。データの順序は逆になることに注意してください。それでは困る場合は reverse を適用してください。数値以外のデータが入力された場合は、エラーメッセージを表示して空リストを返します。

関数 sum-of-list はリストの要素の合計値を求めます。そして、関数 mean-sd で平均値と標準偏差を求めます。平均値は sum-of-list の返り値をリスト ls の長さで割り算すれば求めることができます。ls の長さを局所変数 n に、平均値を m にセットします。そして、リスト ls から要素を取り出して分散 s を計算します。ls が空リストになったら、平均値と標準偏差を計算し、リストに格納して返します。

それでは、実際に実行してみましょう。

$ gosh mean_sd.scm
1 2 3 4 5 6 7 8 9 10
^D を入力する
(5.5 2.8722813232690143)

ところで、参考文献 1 によると、データを 1 回通読するだけで平均値と標準偏差 (分散) を求めることができるそうです。参考文献 1 のプログラムを Scheme で書き直すと、次のようになります。

リスト : 平均値と標準偏差 (2)

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

(define (mean-sd2)
  (let loop ((n 0) (m 0.0) (s 0.0) (x (read)))
    (if (eof-object? x)
        (list m (sqrt (/ s n)))
        (let ((x1 (- x m)) (n1 (+ n 1)))
          (loop
           n1
           (+ m (/ x1 n1))
           (+ s (/ (* n x1 x1) n1))
           (read))))))

;;; 実行
(display (mean-sd2))
(newline)

関数 mead-sd2 は、現在まで読み込んだデータの平均値を仮平均とする方法です。x1 は仮平均 m と x の差を表しています。n1 は読み込んだデータ数です。仮平均 m は x1 / n1 を加算することで更新することができます。同様に分散 s も仮平均を使って計算します。結果は mead-sd とほぼ同じになります。なお、このプログラムはデータのエラーチェックを省略しています。興味のある方はエラー処理を追加してみてください。

●ファイルを行単位で連結する

次はファイルを行単位で連結するプログラムを作りましょう。プログラム名は paste.scm としました。

  file1         file2         標準出力
--------      --------       ----------
  abcd          ABCD          abcdABCD
  efgh    +    EFGH    ==>   efghEFGH
  ijkl          IJKL          ijklIJKL


    図 : 行単位でファイルを連結する

上図に示すように、2 つのファイル file1 と file2 の各行を連結して、標準出力へ書き出します。この場合は、2 つのファイルを同時にオープンしなければいけません。近代的なプログラミング言語では、一度に複数のファイルを扱うことが可能です。

上図に示すように、2 つのファイルを同時にオープンして、作られた入力ポートを別々の局所変数にセットします。変数 in1 に read-line を適用すれば、ファイル 1 から 1 行分データをリードすることができます。同様に、変数 in2 にread-line を適用すれば、ファイル 2 からデータをリードできるのです。あとは、文字列を 2 つ続けて標準出力へ出力すればいいわけです。

ただし、ひとつだけ注意点があります。それは、2 つのファイルの行数は同じとは限らないということです。つまり、どちらかのファイルが先に終了する場合があるのです。この場合は、残ったファイルをそのまま出力します。処理内容を図に示すと、次のようになります。

read-line が返す文字列は改行が取り除かれているので、2 つのファイルから読み込んだデータをそのまま出力し、最後に改行を出力すれば行を連結することができます。ファイル 1 が終了した場合は、ファイル 2 をそのまま出力します。ファイル 2 が終了した場合は、ファイル 1 をそのまま出力しますが、その前に出力したファイル 1 のデータが残っているので、改行を出力することをお忘れなく。

それでは、行を結合する関数 paste を作ります。

リスト : 行の結合 (paste.scm)

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

;;; ファイルをすべて出力
(define (output-file iport)
  (let ((buff (read-line iport)))
    (unless
     (eof-object? buff)
     (display buff)
     (newline)
     (output-file iport))))

;;; 1 行出力
(define (output-line iport)
  (let ((buff (read-line iport)))
    (if (eof-object? buff)
        #f
        (display buff))))

;;; 行の結合
(define (paste filename1 filename2)
  (let ((in1 (open-input-file filename1))
        (in2 (open-input-file filename2)))
    (let loop ()
      (cond
       ((not (output-line in1))
        (output-file in2))
       ((not (output-line in2))
        (newline)
        (output-file in1))
       (else
        (newline)
        (loop))))
    (close-input-port in1)
    (close-input-port in2)))

;;; 実行
(let ((args (command-line)))
  (paste (list-ref args 1) (list-ref args 2)))

関数 paste の引数 filename1 と filename2 は行を連結するファイル名です。次に、open-input-file でファイルをオープンし、入力ポートを局所変数 in1 と in2 にセットします。それから、名前付き let で繰り返しに入ります。

関数 output-line は入力ポートから 1 行読み込んで、それを標準出力へ出力します。もしも、ファイルが終了したら #f を返します。Gauche の場合、display の返り値は #<undef> で、この値は真と判定されます。output-line の返り値をチェックして、 偽であればファイルが終了したことがわかります。

ファイル in1 が終了した場合、関数 output-file でファイル in2 をすべて出力します。ファイル in2 が終了した場合は、改行文字を出力してから output-file でファイル in1 をすべて出力します。そうでなければ、改行文字を出力して loop を再帰呼び出しします。最後に close-input-port で in1 と in2 をクローズします。

関数 output-file は簡単ですね。iport から read-line で 1 行ずつ読み込み、それを display で出力するだけです。このとき、改行文字を出力することをお忘れなく。

list-ref はリストの n 番目の要素を取り出す関数です。

引数 list の長さを n とすると、index には 0 から n - 1 までの値を指定します。リストの先頭の要素が 0 番目になります。簡単な使用例を示しましょう。

gosh[r7rs.user]> (list-ref '(a b c d) 0)
a
gosh[r7rs.user]> (list-ref '(a b c d) 3)
d

引数 args の先頭要素は実行ファイル名なので、list-ref で 1 番目の要素と 2 番目の要素を取り出して paste に渡します。これでプログラムは完成です。実際に試してみてください。

●オプションの取得

ところで、シェルで実行するコマンドには「オプション (option)」といって、機能を指定できるものがあります。オプションは、辞書を引くと「選択」という意味があります。コマンドにいくつかの機能を持たせておいて、ユーザーは自分の使いたい機能をオプションで選択するわけです。Windows では / の後ろの 1 文字で表しますが、Unix 系の OS では - の後ろの 1 文字で表すか、-- の後ろの文字列で表す場合が多いようです。

Gauche にはコマンドラインで指定されたオプションを解析するライブラリが用意されていますが、オプションの有無を調べるだけならば、私たちでも簡単にプログラムを作ることができます。

一番簡単な方法は関数 command-line で取得したパラメータを、オプションとそれ以外のものの 2 つに分けることです。オプションの有無は関数 member で簡単に調べることができますし、オプション以外のパラメータも簡単に取り出すことができます。

プログラムは簡単です。次のリストを見てください。

リスト : オプションの取得と削除 (test.scm)

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

;;; オプションを集める
(define (get-option ls)
  (cond
   ((null? ls) '())
   ((char=? #\- (string-ref (car ls) 0))
    (cons (car ls) (get-option (cdr ls))))
   (else
    (get-option (cdr ls)))))

;;; オプションを削除する
(define (remove-option ls)
  (cond
   ((null? ls) '())
   ((char=? #\- (string-ref (car ls) 0))
    (remove-option (cdr ls)))
   (else
    (cons (car ls) (remove-option (cdr ls))))))

;;; 実行
(let ((args (command-line)))
  (write (get-option args))
  (newline)
  (write (remove-option args))
  (newline))

関数 get-option は文字 - で始まる文字列をオプションとして認識し、それをリストに格納して返します。Lisp / Scheme の場合、文字列は文字が連続したデータとして扱うことができます。つまり、文字列の要素は文字であり、関数 string-ref で文字列から文字を取り出すことができます。

文字列の長さを n とすると、index には 0 から n - 1 までの値を指定します。文字列の先頭が 0 になります。

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

gosh[r7rs.user]> (string-ref "abc def" 0)
#\a
gosh[r7rs.user]> (string-ref "abc def" 3)
#\space
gosh[r7rs.user]> (string-ref "abc def" 6)
#\f

要素の先頭文字が #\- と等しい場合はオプションです。get-option の返り値に要素を追加して返します。そうでなければオプションではないので get-option を再帰呼び出しするだけです。関数 remove-option は get-option の逆の操作で、ls からオプションを取り除いたリストを返します。要素の先頭文字が #\- と等しい場合は remove-option を再帰呼び出しするだけです。そうでない場合は、要素を remove-option の返り値の先頭に追加して返します。

最後にテストの実行例を示します。

gosh[r7rs.user]> gosh test.scm -a abc -b def -c
("-a" "-b" "-c")
("test.scm" "abc" "def")

オプションのリストを opt-list とすると、-a が指定されているか調べるには、(member "-a" opt-list) とすればいいわけです。ファイル名を取り出すにしても、remove-option でオプションが取り除かれているので、1 番目と 2 番目のパラメータをファイル名として扱うことができます。

●行番号を表示する

次は行番号を付けて行を連結してみましょう。プログラムは簡単です。次のリストを見てください。

リスト : 行番号を表示する (paste.scm)

;;; 行番号つきでファイルを出力
(define (output-file-with-number iport n)
  (let ((buff (read-line iport)))
    (unless 
     (eof-object? buff)
     (format #t "~6D:~A~%" n buff)
     (output-file-with-number iport (+ n 1)))))

;;; 行番号つきで 1 行出力
(define (output-line-with-number iport n)
  (let ((buff (read-line iport)))
    (if (eof-object? buff)
        #f
        (format #t "~6D:~A" n buff))))

;;; 行番号つきで行を連結する
(define (paste-with-number filename1 filename2)
  (let ((in1 (open-input-file filename1))
        (in2 (open-input-file filename2)))
    (let loop ((n 1))
      (cond
        ((not (output-line-with-number in1 n))
         (output-file-with-number in2 n))
        ((not (output-line in2))
         (newline)
         (output-file-with-number in1 (+ n 1)))
        (else
         (newline)
         (loop (+ 1 n)))))
    (close-input-port in1)
    (close-input-port in2)))

関数 output-file-with-number と関数 output-line-number は行番号を付けてデータを出力します。引数 n が行番号を表します。行番号の表示は format を使うと簡単ですね。書式文字列に "~6D:~A~% と指定すれば、行番号付きで buff を出力することができます。

関数 paste-with-number は、行番号を名前付き let の局所変数 n で管理します。ファイル in1 のデータを出力するときは output-line-with-number を呼び出しますが、ファイル in2 のデータを出力するときは、行番号を表示する必要はないので output-line を呼び出します。ファイル in2 をすべて出力するときは output-file-with-number を呼び出します。ファイル in2 が終了したときは改行を出力するので、output-file-with-number を呼び出すときは行番号 n を +1 することをお忘れなく。

最後に、オプション -n が指定されていたら行番号を表示するようにします。

リスト : プログラムの実行

(let* ((xs (command-line))
       (option (get-option xs))
       (args (remove-option xs)))
  (if (member "-n" option)
      (paste-with-number (list-ref args 1) (list-ref args 2))
      (paste (list-ref args 1) (list-ref args 2))))

get-option と remove-option でオプションと他のパラメータを分離します。次に、member で option に "-n" があるかテストします。もしあれば、行番号を表示するために paste-with-number を呼び出します。そうでなければ paste を呼び出します。局所変数 args には、オプションを削除したリストがセットされているので、1 番目と 2 番目の要素をファイル名として扱うことができます。

これでプログラムは完成です。オプション -n を指定したら行番号を表示するか、実際に試してみてください。

●文字列ポート

ところで、ポートの入出力はキーボードからの入力や画面への出力、ファイル入出力だけに限ったものではありません。R7RS-small には文字列をポートとして扱う機能が用意されています。これを「文字列ポート」といいます。文字列ポートを作成すると、そのポートに対して入出力関数をそのまま適用することができます。

文字列ポートをオープンするには次の関数を用います。

open-input-string は引数の文字列 string に対応する入力ポートを生成して返します。open-output-string は文字列に対応する出力ポートを生成して返します。簡単な使用例を示しましょう。

gosh[r7rs.user]> (define iport (open-input-string "123 abc def\n"))
iport
gosh[r7rs.user]> iport
#<iport (input string port) XXXXXXXX>
gosh[r7rs.user]> (read iport)
123
gosh[r7rs.user]> (read-char iport)
#\space
gosh[r7rs.user]> (read-char iport)
#\a
gosh[r7rs.user]> (read iport)
bc
gosh[r7rs.user]> (read-line iport)
" def"
gosh&g; (read iport)
#<eof>
gosh[r7rs.user]> (define oport (open-output-string))
oport
gosh[r7rs.user]> oport
#<oport (output string port) XXXXXXXX>
gosh[r7rs.user]> (write 123 oport)
#<undef>
gosh[r7rs.user]> (write-char #\space oport)
#<undef>
gosh[r7rs.user]> (write 'abc oport)
#<undef>
gosh[r7rs.user]> (get-output-string oport)
"123 abc"
gosh[r7rs.user]> (display " def\n" oport)
#<undef>
gosh[r7rs.user]> (get-output-string oport)
"123 abc def\n"

関数 get-output-string は出力ポートに書き込まれたデータを文字列として返します。出力ポートの状態は変化しないので、その後も続けてデータを書き込むことができます。

●バイナリポート

今まで説明したポートは「テキストポート (text port)」といって、read-char や write-char などを用いた文字ベースでの入出力を行います。このほかに、R7RS-small にはバイト (0 - 255) ベースで入出力を行う「バイナリポート (binary port)」が用意されています。

ファイルのオープンは次の関数で行います。

ファイルのクローズは今までと同じです。ポートの種別は次の述語でチェックすることができます。

バイト単位の入出は次の関数を使います。

read-u8 は入力ポートから 1 バイト読み込んで返します。write-u8 は出力ポートに引数 byte を書き込みます。

なお、Unix 系 OS ではテキストポートとバイナリポートの区別はありません。次の例を見てください。

gosh[r7rs.user]> (define a (open-input-file "test1.txt"))
gosh[r7rs.user]> (binary-port? a)
#t
gosh[r7rs.user]> (textual-port? a)
#t

gosh[r7rs.user]> (define b (open-binary-input-file "test2.txt"))
b
gosh[r7rs.user]> b
gosh[r7rs.user]> (binary-port? b)
#t
gosh[r7rs.user]> (textual-port? b)
#t

テキストポートでもバイナリポートでも、textual-port? と binary-port? は真 (#t) を返します。文字で入出力を行う場合は read-char, write-char を、バイトで入出力を行う場合は read-u8, write-u8 を使うだけです。他の OS で動作する Scheme 処理系では、テキストポートとバイナリポートを区別しているかもしれません。

簡単な例として、ファイルをコピーするプログラムを作ってみましょう。次のリストを見てください。

リスト : ファイルのコピー (cp.scm)

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

(define (cp iport oport)
  (let ((c (read-u8 iport)))
    (unless
     (eof-object? c)
     (write-u8 c oport)
     (cp iport oport))))

;;; 実行
(let* ((args (command-line))
       (iport (open-binary-input-file  (list-ref args 1)))
       (oport (open-binary-output-file (list-ref args 2))))
  (cp iport oport)
  (close-input-port iport)
  (close-output-port oport))

関数 cp でファイルをコピーします。引数 iport が入力ポート、oport が出力ポートです。read-u8 で iport から 1 バイト読み込み、それを変数 c にセットします。c が EOF オブジェクトでなければ、それを write-u8 で oport に書き込み、cp を再帰呼び出しするだけです。これでファイルをコピーすることができます。

このほかに、R7RS-small には「バイトベクタ (bytevector)」を使った入出力関数も用意されています。バイトベクタは R7RS-samll で定義されているデータ型です。これは「ベクタ (vector)」を取り上げるときに説明することにします。

●まとめ

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

  1. 関数 list-ref はリストの n 番目の要素を取り出す。
  2. 関数 string-ref は文字列の n 番目の文字を取り出す。
  3. 文字列ポートは文字列に対して入出力を行う。
  4. 文字列ポートの作成は open-input-string, open-output-string を使う。
  5. get-output-string は出力ポートに書き込まれた文字列を返す。
  6. バイナリポートは関数 open-binary-input-file, open-binary-output-file でオープンする。
  7. バイト単位の入出力は関数 read-u8, write-u8 を使う。

いよいよ次回から Scheme プログラミング中級編に入ります。Lisp / Scheme らしいプログラミングに挑戦してみましょう。お楽しみに。

●参考文献

  1. 奥村晴彦, 『C言語による最新アルゴリズム事典』, 技術評論社, 1991

●プログラムリスト

;;;
;;; paste.scm : ファイルを行単位で連結する
;;;
;;;             Copyright (C) 2007-2020 Makoto Hiroi
;;;
(import (scheme base) (scheme read) (scheme write) (scheme file)
        (scheme process-context) (gauche base))

;;; 行番号つきでファイルを出力
(define (output-file-with-number iport n)
  (let ((buff (read-line iport)))
    (unless 
     (eof-object? buff)
     (format #t "~6D:~A~%" n buff)
     (output-file-with-number iport (+ n 1)))))

;;; 行番号つきで 1 行出力
(define (output-line-with-number iport n)
  (let ((buff (read-line iport)))
    (if (eof-object? buff)
        #f
        (format #t "~6D:~A" n buff))))

;;; 行番号つきで行を連結する
(define (paste-with-number filename1 filename2)
  (let ((in1 (open-input-file filename1))
        (in2 (open-input-file filename2)))
    (let loop ((n 1))
      (cond
        ((not (output-line-with-number in1 n))
         (output-file-with-number in2 n))
        ((not (output-line in2))
         (newline)
         (output-file-with-number in1 (+ n 1)))
        (else
         (newline)
         (loop (+ 1 n)))))
    (close-input-port in1)
    (close-input-port in2)))

;;; ファイルをすべて出力
(define (output-file iport)
  (let ((buff (read-line iport)))
    (unless
     (eof-object? buff)
     (display buff)
     (newline)
     (output-file iport))))

;;; 1 行出力
(define (output-line iport)
  (let ((buff (read-line iport)))
    (if (eof-object? buff)
        #f
        (display buff))))

;;; 行の連結
(define (paste filename1 filename2)
  (let ((in1 (open-input-file filename1))
        (in2 (open-input-file filename2)))
    (let loop ()
      (cond
       ((not (output-line in1))
        (output-file in2))
       ((not (output-line in2))
        (newline)
        (output-file in1))
       (else
        (newline)
        (loop))))
    (close-input-port in1)
    (close-input-port in2)))

;;; オプションを集める
(define (get-option ls)
  (cond
   ((null? ls) '())
   ((char=? #\- (string-ref (car ls) 0))
    (cons (car ls) (get-option (cdr ls))))
   (else
    (get-option (cdr ls)))))

;;; オプションを削除する
(define (remove-option ls)
  (cond
   ((null? ls) '())
   ((char=? #\- (string-ref (car ls) 0))
    (remove-option (cdr ls)))
   (else
    (cons (car ls) (remove-option (cdr ls))))))

;;; 実行
(let* ((xs (command-line))
       (option (get-option xs))
       (args (remove-option xs)))
  (if (member "-n" option)
      (paste-with-number (list-ref args 1) (list-ref args 2))
      (paste (list-ref args 1) (list-ref args 2))))

●問題

次に示すプログラムを作成してください。

  1. ファイルの先頭 10 行を表示するプログラム head.scm
  2. ファイルの末尾 10 行を表示するプログラム tail.scm
  3. ファイルの行数と単語数をカウントするプログラム wc.scm












●解答1

リスト : ファイルの先頭 10 行を表示する (haed.scm)

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

(define (head iport)
  (let loop ((n 0))
    (let ((buff (read-line iport)))
      (cond
       ((>= n 10) #t)
       ((eof-object? buff) #f)
       (else
        (display buff)
        (newline)
        (loop (+ n 1)))))))

;;; 実行
(let ((args (cdr (command-line))))
  (if (zero? (length args))
      (head (current-input-port))
      (call-with-input-file (car args) head)))

最初にコマンドラインからファイル名を取得します。ファイル名が無い場合は標準入力からデータを読み込みます。実際の処理は関数 head で行います。read-line で 1 行読み込み、それを display で表示します。10 行表示する、または、途中でファイルの終端に到達したら終了します。

●解答2

リスト : ファイルの末尾 10 行を表示する (tail.scm)

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

(define (tail iport)
  (let loop ((q '()))
    (let ((buff (read-line iport)))
      (cond
       ((eof-object? buff)
        (for-each (lambda (x) (display x) (newline)) q))
       ((< (length q) 10)
        (loop (append q (list buff))))
       (else
        (loop (append (cdr q) (list buff))))))))

;;; 実行
(let ((args (cdr (command-line))))
  (if (zero? (length args))
      (tail (current-input-port))
      (call-with-input-file (car args) tail)))

実際の処理は関数 tail で行います。read-line で 1 行読み込み、変数 q に直近の 10 行を格納します。ファイルの終端に到達したら、for-each で q に格納した行を表示します。q の長さが 10 未満の場合、q の末尾に読み込んだデータ buff を追加します。q の長さが 10 の場合は、先頭データを削除してから buff を末尾に追加します。

簡単なプログラムですが、リストの末尾にデータを追加するとき append を使っているので、効率の良いプログラムではありません。「キュー (queue)」というデータ構造を使うと、もっと効率的なプログラムに改良することができます。キューはあとの回で取り上げます。

●解答3

リスト : ファイルの行数と単語数をカウントする (wc.scm)

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

(define (wc iport)
  (let loop ((word 0) (line 0) (inword #f))
    (let ((c (read-char iport)))
      (cond
       ((eof-object? c)
        (display (list line word))
        (newline))
       ((char-whitespace? c)
        (loop word
              (if (char=? #\newline c) (+ line 1) line)
              #f))
       ((not inword)
        (loop (+ word 1) line #t))
       (else
        (loop word line inword))))))

;;; 実行
(let ((args (cdr (command-line))))
  (if (zero? (length args))
      (wc (current-input-port))
      (call-with-input-file (car args) wc)))

単語は空白文字で区切られた文字列とします。単語数は変数 word に、行数は変数 line に格納します。read-char で 1 文字読み込み、それを変数 c にセットします。c が EOF オブジェクトであれば、display で line を word を表示します。

c が空白文字の場合は inword を #f にします。ここで、c が改行文字であれば line の値を +1 します。そして、inword を #f にセットします。c が空白文字以外の場合、inword が #f であれば #t に変更します。このとき、単語の個数 word を +1 します。空白文字のチェックはライブラリ (scheme char) の関数 char-whitespace? で簡単に行うことができます。


初版 2008 年 1 月 5 日
改訂 2020 年 8 月 30 日

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

[ PrevPage | Scheme | NextPage ]