[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

13. カウント:繰り返しと正規表現

繰り返しと正規表現の検索は、Emacs Lisp でコードを書く際によく使われる大 変強力な道具である。この章では、正規表現の検索の利用の仕方を、 while ループないしは再帰を利用した単語を数えるコマンドの作成を通 して説明していくことにする。

単語のカウント  Emacs には単語を数えるコマンドが欠けている
13.1 関数 count-words-region  正規表現を使う。でも問題が...
13.2 再帰を使った単語数のカウント  
13.3 練習問題:句読点のカウント  

単語のカウント

標準的な Emacs の配布にはリージョン内の行数を数える関数が含まれている。 しかし同じことを単語について行う関数はない。

文章の種類によっては単語の数を数えなければならないようなことがある。エッ セイを書く場合は800単語以内に制限した方がよいであろうし、小説を書く場合 など、一日1000単語は書くぞと決心することもあるだろう。個人的には Emacs に単語を数えるコマンドがないのは変だと思う。多分 Emacs を使う人は大抵コー ドとかドキュメントを書いていて、単語の数を数える必要などないのだろう。あ るいは Operating system 附属の単語数を数えるコマンドである wc し か使わなかったりするのかもしれない。はたまた編集者の都合に合わせて、文書 の中の単語の数を文字数を5で割ったものとして数えている人もいるかもしれな い。何はともあれ、以下で単語数を数えるコマンドを紹介することにする。


13.1 関数 count-words-region

単語数を数えるコマンドとしては、行、パラグラフ、あるいはリージョンやバッ ファなどの中に含まれる単語の数を数えるものが考えられる。コマンドとしては どんなことまで出来ればよいだろうか。コマンドをバッファ全体の単語を数える ように設計することも出来るが、Emacs の伝統からいって、もっと柔軟性を持た せた方がよいだろう。例えばバッファ全体ではなく、あるセクションの中の単語 の数を教えるようなコマンドが欲しい場合も出てくるかもしれない。従って、リー ジョンの中の単語数を数えるように設計した方が合理的だろう。一度、このよう な count-words-region というコマンドを作ってしまえば、C-x h (mark-whole-buffer) を使ってバッファ全体をマークすることで、バッ ファ全体の単語を数えることだって出来る。

単語を数えることは、明らかに繰り返しを伴う動作だ。リージョンの始まりから スタートして最初の単語を数え、次に二番目に行き、三番目に行き、というふう にしてリージョンの最後に来るまで続けるわけである。これは単語を数えるとい う行為が、もともと再帰や while ループに適しているということを意味 する。

まずは、while ループを使って、次に再帰を使って、単語を数えるコマ ンドを実際に作ってみることにする。これらのコマンドは勿論インタラクティブ であるべきである。

これまでにも何度も見たように、インタラクティブな関数の定義のテンプレート は次のようになっている。

 
(defun 関数名 (引数リスト)
  "説明文字列..."
  (インタラクティブ式...)
  本体...)

このスロットを埋めればよい。

関数の名前はそれ自体で意味が通じ、かつ既にある 関数 count-lines-region の名前と似たものであるべきである。これは 名前を覚えやすくするためだ。count-words-region なんかが良いだろう。

この関数はリージョン内の単語の数を数える。従って引数リストには、リージョ ンの始まりと終わりの二つの位置各々にバインドされるシンボルが含まれていな ければならない。これらの二つの位置は `beginning'`end' と呼 べばいいだろう。説明文字列の最初の行は一つの文であるべきである。というの は apropos のようなコマンドで表示される部分は一行目が全てであるか らである。インタラクティブ式は `(interactive "r")' という形になる。 こうすることで、リージョンの始まりと終わりの位置をこの関数の引数リストに 渡すことが出来る。ここまではルーティーンワークである。

関数の本体は、三つの仕事をするように書かれなければならない。一番目は while ループが単語を数える時の条件を設定すること、二番目は while ループを走らせること、そして三番目はユーザにメッセージを送 ることである。

ユーザが count-words-region を呼び出した時点では、ポイントはリー ジョンの始まり、もしくは終わりにある。しかし、カウントは常にリージョンの 始まりの位置から開始しなければならない。従って、そうでない場合はまずその 位置までポイントを移動しておいて欲しい。(goto-char beginning) を 実行することで、このことが保証される。勿論、関数が仕事を終えたらポイント の位置は元の位置に戻っていて欲しい。このために、本体部分は save-excursion 式で囲む必要がある。

本体の中心部分は、あるS式で次の単語に一つずつジャンプし、もう一つのS式 でジャンプの回数を数える while ループからなる。while ルー プの真偽テストはポイントがもう先へは進めなくなるまでは真を返し、リージョ ンの最後まできたら偽を返すようなものでなければならない。

ポイントを単語ごとに移動するS式としては (forward-word 1) を使っ てもよい。しかし、正規表現の検索を使うなら、Emacs が何を単語と見倣すかを 見る方が簡単である。

単語のパターンを検索する正規表現の検索では、ポイントはマッチしたパターン の最後の文字の位置に移動する。従って、続けて検索が成功している間はポイン トは単語を一つずつ移動していくことになる。

現実問題として、正規表現の検索では、単語だけではなく単語間の空白や句読点 をジャンプするようになっていて欲しい。実際、単語間の空白を越えて進まない ような正規表現では二単語以上先には進めなくなってしまう。つまり、正規表現 の中には単語だけでなく、その後に続く空白や句読点等も含まれていなければな らないわけである。(単語はバッファの最後に来るかもしれないので、後に必ずし も空白がくるとは限らない。従って、空白等の部分の正規表現はオプションに なる。)

というわけで、我々が求めている正規表現は、まず幾つかの単語構成文字が続き、 オプションとして単語の構成要素以外の文字が一つ以上の続くようなパターンで ある。このような正規表現は次のように書ける。

 
\w+\W*

どの文字が単語構成文字であり、どの文字がそうでないかは、そのバッファのシ ンタックステーブルによる。 (詳しくは 単語やシンボルは何から構成されているか, あるいは section `The Syntax Table' in The GNU Emacs Manual, や section `Syntax Tables' in The GNU Emacs Lisp Reference Manual, を参照。)

検索のためのS式は次の通りである。

 
(re-search-forward "\\w+\\W*")

(`w'`W' の前にはバックスラッシュが二つずつ組になっているこ とに注意しよう。単独のバックスラッシュは Emacs インタプリタにとって特殊 な意味を持っている。つまり、後に続く文字が普通とは異なる意味に解釈される のである。例えば、`\n' という二つの文字は、バックスラッシュに続く `n' ではなく、改行コードを表わす。バックスラッシュが二つ続いて初め て通常の「特殊でない」バックスラッシュを表わすのである。)

(訳註:つまり、バックスラッシュが二つ要るのは、それが一度 Emacs の Lisp リーダによって一つのバックスラッシュに解釈された後の結果が正規表現として 解釈されるためである。ここは混乱しやすいので注意しよう。)

いくつ単語があったかを数えるためのカウンタも必要である。この変数は最初は0 にセットされ、while ループを一つ繰り返す度に一つずつ増えていく。この一つ 増やすためのS式は単に

 
(setq count (1+ count))

だけでよい。さて、最後にユーザに対してリージョンにいくつ単語があったかを 知らせたい。このようにユーザに情報を表示する目的には、message 関 数が用意されている。今の場合、メッセージは単語の数がいくつあった場合でも きちんとした文章になっていなければならない。例えば、"there are 1 words in the region" というふうになっては困る。単語が一つなのに複数形が使われ るのは文法的に正しくないからである。この問題は、条件分岐式を用いて単語数 によってメッセージを変えるようにすれば回避出来る。可能性は三つある。一つ はリージョン内には全く単語が無い場合、もう一つは一個だけある場合、そして、 複数の単語がある場合である。このような場合には cond を使うとよい のであった。

以上を総合すると、次のような関数定義になる。

 
;;; 最初のバージョン: バグあり!
(defun count-words-region (beginning end)  
  "Print number of words in the region.
Words are defined as at least one word-constituent
character followed by at least one character that
is not a word-constituent.  The buffer's syntax
table determines which characters these are."
  (interactive "r")
  (message "Counting words in region ... ")

;;; 1. 適切な状況の設定。
  (save-excursion
    (goto-char beginning)
    (let ((count 0))

;;; 2.  while loop を走らせる。 
      (while (< (point) end)
        (re-search-forward "\\w+\\W*")
        (setq count (1+ count)))

;;; 3. ユーザにメッセージを送る。
      (cond ((zerop count)
             (message 
              "The region does NOT have any words."))
            ((= 1 count) 
             (message 
              "The region has 1 word."))
            (t 
             (message 
              "The region has %d words." count))))))

が、以下に書かれている通り、この関数は全ての状況できちんと動作するわけでは ない。

13.1.1 count-words-region の空白文字に関するバグ  


13.1.1 count-words-region の空白文字に関するバグ

前節で説明した count-words-region コマンドには、二つのバグ、とい うか二つの現れ方をする一つのバグがある。まず、たとえリージョンがある文章 の途中にある空白文字しか含まない場合でも、count-words-region は単 語が一つあると言ってくる。また、バッファないしはアクセス可能な部分の最後 にある空白文字しか含まない場合は、

 
Search failed: "\\w+\\W*"

というエラーメッセージが返ってしまう。この文を GNU Emacs の Info で読ん でいるなら、このバグを実際に自分で確かめることが出来る。

まずはこの関数をいつも通り評価してインストールしよう。 ここに定義のコピーを書いておく。最後の閉じ括弧の直後にカーソルを持っていっ て C-x C-e とタイプすればインストールされる。

 
;; 最初のバージョン; バグあり!
(defun count-words-region (beginning end)  
  "Print number of words in the region.
Words are defined as at least one word-constituent character followed
by at least one character that is not a word-constituent.  The buffer's
syntax table determines which characters these are."
  (interactive "r")
  (message "Counting words in region ... ")

;;; 1. 適切な状況の設定。
  (save-excursion
    (goto-char beginning)
    (let ((count 0))

;;; 2.  while loop を走らせる。
      (while (< (point) end)
        (re-search-forward "\\w+\\W*")
        (setq count (1+ count)))

;;; 3. ユーザにメッセージを送る。
      (cond ((zerop count)
             (message "The region does NOT have any words."))
            ((= 1 count) (message "The region has 1 word."))
            (t (message "The region has %d words." count))))))

お望みなら、次の式を評価することでこのコマンドのためのキーバインディン グもインストール出来る。

 
(global-set-key "\C-c=" 'count-words-region)

最初のテストをするために、次の行の最初と最後にマークとポイントを設定して C-c = (あるいは C-c = というキーバインディングをインストール していなければ M-x count-words-region) とタイプしよう。

 
    one   two  three        

Emacs はリージョンには三つの単語があると正しく答えるはずだ。

次に、マークを行頭にセットして、ポイントをちょうど `one' という単語 のに置いて同じテストをしてみよう。C-c = (あるいは M-x count-words-region) とタイプした場合、Emacs はリージョンには一 つも単語がないと答えるべきである。リージョンには行頭の空白文字だけしか含 まれないからだ。しかし実際は Emacs はリージョンには一つの単語があると答 えてくる!

三番目に、サンプルの行を `*scratch*' バッファの最後にコピーして行の 終わりに幾つか空白文字をタイプしてから、マークを `three' の直後に、 ポイントを行末に設定する。(即ち、行末をバッファの最後にする。) ここでさっ きと同じく C-c = (あるいは M-x count-words-region) とタイプ しよう。空白文字しかないのだから、今回も Emacs は単語は一つもないと答え るべきである。しかし、Emacs は `Search failed' というエラーメッセー ジを表示する。

この二つのバグは共通する問題から生じたものだ。

最初のバグを考えてみよう。コマンドを実行すると、空白だけしかないはずの行 頭部分に一つの単語が含まれていると言ってきたのであった。これは何故だろう か? M-x count-words-region コマンドは、まずポイントをリージョン の最初に移動させる。while テストは point の値が end の値よりも小さいかどうかテストする。これは正しいので、結果として最初の単 語を見つける正規表現の検索が実行され、最初の単語が見つかる。ポイントは単 語の後ろに移され、count は1にセットされる。while ループは もう一度実行されるが、今度はポイントの値が end の値よりも大きくな るので、ループはそこで終了する。そしてこの関数は、リージョン内の単語は一 つだと言うメッセージを表示するというわけである。手短にいうと、正規表現の 検索では、単語がリージョンの外にあるにも関わらず、単語を発見してしまうの である。

二番目のバグの場合、リージョン内にはバッファの最後の空白文字だけしかなかっ た。Emacs は `Search failed' と言う。これは何故か? まず最初の while ループでは真偽テストは真になるので、検索が実行される。しか し、バッファの最後には単語は一つもないので、検索は失敗するというわけである。

どちらの現象も、検索がリージョンを越えて実行される所に問題があった。

解決法は、検索をリージョンに制限することである---これはかなり単純な動作 に思えるが、実際に考えてみると、思った程簡単にはいかないことが分る。

今まで見てきたように、re-search-forward 関数は最初の引数を検索 パターンとする。しかし、この最初の必須の引数の他に、三つの省略可能な引 き数を取ることが出来る。省略可能である二番目の引数は検索の限界である。 オプションである三番目の引数は、もし t なら検索に失敗した場合に エラーを出さずに nil を返す。オプションである四番目の引数には繰 り返しの回数を指定する。(Emacs では、関数定義の説明文字列を見るには C-h f に続けて関数名をタイプして RET キーを押せばよい。)

count-words-region の定義内では、変数 end にリージョンの 最後の値がこの関数の引数として渡され保持される。そこで、正規表現の検索の 引数として、この end の値を与えてみよう。

 
(re-search-forward "\\w+\\W*" end)

しかし、もし count-words-region の値にこの変更だけしか加えずに、 新しいバージョンの関数を、空白だけの場所でテストすると、`Search failed' というメッセージが返される。

これはどうしてかというと、リージョン内には単語を構成する文字が無いために、 検索をそこに制限すると、検索が失敗してしまうためである。そのためにエラー メッセージが返される。しかし、この場合にエラーメッセージは受け取りたくな い。期待されるのは "The region does NOT have any words." というメッセー ジである。

この問題を回避するには re-search-forward に三番目の引数として t を与えれば良い。すると関数はエラーメッセージの代わりに nil を返す。

しかし、この変更を加えてからもう一度テストしてみると、 "Counting words in region ... " というメッセージが表示された後いくら待っ ても何も表示されない。結局 C-g (keyboard-quit) とタイプする までずっとこのメッセージを見続けることになる。

どうしてこうなるかだが、検索はリージョンに制限されているのでさっきと同じ ように失敗する。これはリージョンには単語を構成する文字が含まれないのだか ら期待通りである。結果として re-search-forward 式は nil を 返す。そして他のことは何もしない。特に、ポイントを動したりもしない。これ は検索に成功した場合の副作用だからだ。re-search-forwardnil を返した後、while ループの次のS式が評価される。これは カウンタを一つ増やす。そして、次のループに移る。re-search-forward 式でポイントが移動しないために、ポイントの値は end の値よりも小さ いままであり、真偽テストは真になる。そしてループが繰り返される。これがずっ と続く ...

検索が失敗した場合は while ループの真偽テストが偽を返すようにする ために、またもや count-words-region の定義に変更を加える必要があ る。即ち、カウンタを一つ増やす前に行う真偽テストでは二つの条件が満たされ なければならないのだ。一つはポイントがリージョン内にあること、もう一つは 検索によって単語が見つかることである。

最初の条件も二番目の条件も同時に真にならなければいけないので、次のように リージョンテストを行うS式と検索を行うS式の二つのS式を and 関数 で連結して while ループの真偽テストに埋め込むことにしよう。

 
(and (< (point) end) (re-search-forward "\\w+\\W*" end t))

(and についての詳細は 12.4 forward-paragraph:関数の金脈, を参照。)

re-search-forward 式は、もし検索が成功した場合は真を返し、副作用 としてポイントを移動する。結果として、単語が見つかった場合にはポイントは リージョンを越えてしまうことがある。検索が失敗した場合やポイントがリージョ ンの終わりに来てしまった場合は、真偽テストは偽を返し、while ルー プが終了して、メッセージが表示される。

これらの最終的な変更を加えると、count-words-region は (少なくとも 私が見た範囲では!) バグ無しに動作するようになる。コードは次の通りである。

 
;;; 最終バージョン: while
(defun count-words-region (beginning end)  
  "Print number of words in the region."
  (interactive "r")
  (message "Counting words in region ... ")

;;; 1. 適切な状況の設定
  (save-excursion
    (let ((count 0))
      (goto-char beginning)

;;; 2.  while loop を走らせる。
      (while (and (< (point) end)
                  (re-search-forward "\\w+\\W*" end t))
        (setq count (1+ count)))

;;; 3. ユーザにメッセージを送る
      (cond ((zerop count)
             (message
              "The region does NOT have any words."))
            ((= 1 count)
             (message
              "The region has 1 word."))
            (t
             (message
              "The region has %d words." count))))))


13.2 再帰を使った単語数のカウント

単語を数える関数は while ループではなく再帰を使っても書くことが出 来る。実際にどのようになるかを見てみよう。

まず、count-words-region 関数は三つの仕事をするということをはっき り認識しておこう。初めに、カウントを行う際の適当な条件を整え、次にリージョ ン内の単語を数え、最後に単語の数を知らせるメッセージをユーザに対し表示す るというわけである。

これらの仕事を全て一つの再帰関数でやらせようとすると、全ての再帰呼び出し でメッセージを受け取ってしまうことになる。例えばリージョンに13の単語があっ た場合、13個のメッセージが並んでしまうのだ。これでは困る。そこで、これら の仕事を二つの関数に分けてやらせることにする。一つを再帰関数にして、それ をもう一方で呼び出すのである。片方は条件を整え、メッセージを表示するもの であり、もう片方は、単語を数えるものである。

まずはメッセージを表示する方の関数から始めよう。今回も関数の名前は count-words-region を使うことにする。

こちらはユーザが呼び出す方の関数であり、インタラクティブなものになる。実 の所、これはこの関数の前回のバージョンにそっくりである。ただし、リージョ ン内の単語を数えるために、途中で recursive-count-words を呼び出し ている所だけが違う。

前回の関数を参考にすれば、簡単にこの関数のテンプレートを作ることが出来る。

 
;; 再帰バージョン; 正規表現検索を利用
(defun count-words-region (beginning end)  
  "説明文字列..."
  (インタラクティブ式...)

;;; 1. 適切な状況の設定
  (状況説明のメーセージ)
  (初期設定のための関数...

;;; 2. 単語数のカウント
    再帰呼び出し

;;; 3. ユーザにメッセージを送る。
    単語数を伝えるメッセージ))

この定義は非常に素直に書かれているが、再帰呼び出しで返されたカウントを単 語の数を表示するメッセージに渡す所が、幾分ややこしいかもしれない。これは、 ちょっと考えれば、let 関数を使えばよいと気付く。つまり、 let 式の変数リストの変数に、再帰関数から返されたリージョン内の単 語数をバインドすればよいのである。その後、cond 式を利用してその値 をユーザに表示する。

let 式の中での変数のバインドというと、何か関数の二義的な仕事に思 われることも多い。しかし、今の場合には、関数の主な仕事だと思われる単語を 数えるということが、let 式の中で行われることになる。

let を使うと、関数定義は次のようになる。

 
(defun count-words-region (beginning end)  
  "Print number of words in the region."
  (interactive "r")

;;; 1. 適切な状況の設定
  (message "Counting words in region ... ")
  (save-excursion
    (goto-char beginning)

;;; 2. 単語数のカウント
    (let ((count (recursive-count-words end)))    

;;; 3. ユーザに対しメッセージを送る
      (cond ((zerop count)
             (message
              "The region does NOT have any words."))
            ((= 1 count)
             (message
              "The region has 1 word."))
            (t
             (message
              "The region has %d words." count))))))

次に、再帰関数の方に移ろう。

再帰関数は少なくとも三つの部分を持たねばならない。`do-again-test'、 `next-step-expression'、そして、再帰呼び出しである。

do-again-test は関数が自分自身をもう一度呼ぶかどうかを決定するものである。 ここではリージョン内の単語を数え、その中の単語の分だけ前に移動するのであ るから、do-again-test ではポイントがまだリージョンの内部にあるかどうかを チェックすれば良い。つまり、ポイントの値を調べて、それがリージョンの終わ りの値よりも小さいか大きいかを比べるわけである。ポイントの値を調べるには point 関数を使えばよい。リージョンの終わりの値は、勿論、この再帰 関数の引数として渡す必要がある。

更に do-again-test では検索で単語が見つかったかどうかもテストしなければ ならない。無かった場合は再帰呼び出しをすべきではない。

next-step-expression は再帰関数が自分自身の呼び出しを止めるべき時に止め るように値を変化させるものである。より正確に言うと、next-step-expression は適切な時に do-again-test が再帰呼び出しを繰り返すのを止めるように、値 を変化させていく。今の場合なら、next-step-expression はポイントを単語ご とに移動させていくS式になる。

再帰関数の三つ目の部分は、再帰呼び出しである。

また、どこかにこの関数の実際の「仕事」をする部分、つまり単語数を数える部 分を書く必要がある。これが一番重要な部分だ!

が、ともかく、再帰関数のアウトラインは出来た。

 
(defun recursive-count-words (region-end)
  "説明文字列..."
   do-again-test
   next-step-expression
   再帰呼び出し)

このスロットを埋めていけばよい。まずは、一番単純な場合から始めよう。もし ポイントがリージョンの終わりもしくはそれ以降の位置にあった場合は、リージョ ン内には単語はない。従って、関数は0を返すべきである。同様に検索が失敗 した場合も単語はないので、0を返すべきである。

一方、もしポイントがリージョンの中にあって検索が成功した場合には、関数は 自分自身を再度呼び出すべきである。

というわけで、do-again-test は次のようになる。

 
(and (< (point) region-end)
     (re-search-forward "\\w+\\W*" region-end t))

検索をするS式も do-again-test の一部であることに注意しよう。この関数は 検索が成功すれば t を返し、失敗すれば nil を返す。 (count-words-region の空白文字に関するバグ, を見れば、re-search-forward がどのように動作するかが分る。)

do-again-test は if 式の真偽テストである。明らかに、もし真偽テス トが真だった場合には、if 式はこの関数を再度呼び出さなければならない。 が、もし偽だった場合には、else-part によって零が返されるべきである。何故 なら、単語が見つからなかったということは、ポイントがリージョンの外だった り検索に失敗したということだからだ。

しかし、再帰呼び出しについて考える前に、next-step-expression について考 える必要があるのだった。これは今の場合何か? 興味深いことに、これは do-again-test の検索部分なのである。

do-again-test で tnil を返すことに加えて、 re-search-forward は検索成功時には副作用としてポイントを前方に移 動する。これこそが、ポイントがリージョン内を移動しきった場合に、再帰関数 が自分自身を呼び出すのを止めるようにポイントの値を変化させていく動作なの である。というわけで、re-search-forward 式が next-step-expression になっているのである。

従って recursive-count-words のアウトラインは次のようになる。

 
(if do-again-test 兼 next-step-expression
    ;; then
    再帰呼び出しをしてカウントを返す
  ;; else
  ゼロを返す)

これにカウントを数えさせるような機構を組み込むには、どうしたらよいだろう か。next-step-expression では一単語ごとに前方に進み、従って再帰呼び出し は一つ単語を進むごとに行われるわけだから、カウントの仕組は recursive-count-words が呼ばれるごとに数を一つ増やすようなもので なければならない。

幾つかの場合を考えてみよう。

以上のことから、if 式の else-part では、単語が一つもなかった場合 のために0を返せばよいことが分る。これは、if 式の then-part では残 りの単語の合計の値に1加えた値を返すべきだということを意味する。

実際のS式は次のようになる。ここで、1+ は引数に1加える関数である。

 
(1+ (recursive-count-words region-end))

以上から recursive-count-words 関数全体は次のようになる。

 
(defun recursive-count-words (region-end)
  "説明文字列..."

;;; 1. do-again-test
  (if (and (< (point) region-end)          
           (re-search-forward "\\w+\\W*" region-end t))

;;; 2. then-part: 再帰呼び出し
      (1+ (recursive-count-words region-end))  

;;; 3. else-part
    0))             

実際にどのような動作をするかを見てみよう。

もし、リージョンに単語が一つもなければ、if 式の else-part が評価 され、結果としてこの関数は0を返す。

もし、リージョンに単語が一つあれば、ポイントの値は region-end の 値よりも小さく、検索は成功する。この場合、if 式の真偽テストは真を 返し、then-part が評価され、カウント式が評価される。この式が返す値が関数 全体の返す値になるのだが、これは、再帰呼び出しで返された値に1加えた値で ある。

ところで、この時同時に next-step-expression によってポイントがリージョン 内の最初の単語 (といっても今はこの一個だけだが) を越えて移動する。つまり、 (recursive-count-words region-end) が二回目に評価された時には、ポ イントの位置はリージョンの終わり以降の位置にあることになる。従って、今度 は recursive-count-words は0を返す。これにさっきの1が加えられ、結 果として元の recursive-count-words は0足す1、つまり、1を返すこと になる。これは正しい数である。

ここまでくれば後はもうお分りだと思うが、リージョンの中に単語が二つある場 合には、最初に呼び出された recursive-count-words は次に残りの単語 を含むリージョンで呼び出された recursive-count-words の値に1加え た値、つまり、1 + 1 = 2 という値を返す。これも正しい数である。

同様にして、もしリージョンの中に単語が三つある場合も、最初に呼び出された recursive-count-words は残りのリージョンで呼び出された recursive-count-words の値に1加えた値を返す。このような感じで、単 語が沢山ある場合も正しい数を返してくれる。

説明文字列も完全につけると、二つの関数は次のようになる。

再帰関数:

 
(defun recursive-count-words (region-end)
  "Number of words between point and REGION-END."

;;; 1. do-again-test
  (if (and (< (point) region-end)
           (re-search-forward "\\w+\\W*" region-end t))

;;; 2. then-part: 再帰呼び出し
      (1+ (recursive-count-words region-end)) 

;;; 3. else-part
    0))             

外側の関数 (wrapper):

 
;;; 再帰バージョン
(defun count-words-region (beginning end) 
  "Print number of words in the region.

Words are defined as at least one word-constituent
character followed by at least one character that is
not a word-constituent.  The buffer's syntax table
determines which characters these are."
  (interactive "r")
  (message "Counting words in region ... ")
  (save-excursion
    (goto-char beginning)
    (let ((count (recursive-count-words end)))
      (cond ((zerop count)
             (message
              "The region does NOT have any words."))
            ((= 1 count)
             (message "The region has 1 word."))
            (t
             (message
              "The region has %d words." count))))))


13.3 練習問題:句読点のカウント

while ループを使って、リージョン内の句読点---終止符、カンマ、セ ミコロン、コロン、感嘆符、疑問符---の数を数える関数を書きなさい。同じ関 数を再帰関数を使って書きなさい。


[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

This document was generated by Matsuda Shigeki on April, 10 2002 using texi2html