count-words-example ¶の設計まず、単語数カウントコマンドをwhileループで、次に再帰で実装します。もちろん、このコマンドは対話型となります。
インタラクティブな関数定義のテンプレートは、いつもの通り以下の通りです:
(defun name-of-function (argument-list) "documentation..." (interactive-expression...) body...)
私たちがすべきことは、空欄を埋めることです。
関数名は自明で覚えやすいものであるべきです。count-words-regionが当然の選択肢でしょう。その名称は単語数を数えるEmacsの標準コマンドに使われているため、我々の実装名はcount-words-exampleとします。
この関数は、領域内の単語数をカウントします。つまり、引数リストには、領域の開始位置と終了位置という2つの位置に束縛されたシンボルが含まれている必要があります。これら2つの位置は、それぞれ
‘beginning’ および ‘end’ と呼ぶことができます。ドキュメントの1行目は単一の文であるべきです。というのも、apropos
のようなコマンドによってドキュメントとして出力されるのはそれだけだからです。インタラクティブな式は ‘(interactive "r")’
という形式になります。これにより、Emacs が領域の開始位置と終了位置を関数の引数リストに渡すようになるためです。これらはすべて定型的な処理です。
関数本体は、3つの処理を行うように記述する必要があります。具体的には、まず、whileループが単語をカウントできる条件を整えること、次に、whileループを実行すること、そして最後に、ユーザーにメッセージを送信することです。
ユーザーが count-words-example を呼び出す際、point
は領域の先頭または末尾にある可能性があります。ただし、カウント処理は領域の先頭から開始しなければなりません。つまり、point
がまだそこにない場合は、そこに配置する必要があります。(goto-char beginning)
を実行することで、これが保証されます。もちろん、関数の処理が完了した際には、point を想定通りの位置に戻す必要があります。このため、本体は
save-excursion 式で囲む必要があります。
関数本体の中核部分は、一方の式がポインタを単語単位で前方へジャンプさせ、もう一方の式がそのジャンプ回数をカウントするwhileループで構成されている。whileループの真偽判定は、ポインタが前方へジャンプすべき限りtrueとなり、ポインタが領域の終端にあるときはfalseとなるべきである。
単語単位でポイントを移動させる式として (forward-word 1)
を使うこともできますが、正規表現検索を使えば、Emacsが何を「単語」として認識しているのかがより分かりやすくなります。
検索対象のパターンが見つかる正規表現検索では、マッチした最後の文字の後にポイントが置かれます。つまり、単語検索が連続して成功すると、ポイントは単語ごとに前方へ移動していきます。
実用上、正規表現による検索では、単語間の空白や句読点だけでなく、単語自体もスキップさせたいと考えます。単語間の空白をスキップしない正規表現では、決して2単語以上をスキップすることはできません!つまり、正規表現には、単語自体に加えて、その後に続く空白や句読点(もしあれば)も含める必要があります。(単語がバッファの末尾となり、その後に空白や句読点が続かない場合もあるため、正規表現のその部分は省略可能である必要があります。)
したがって、正規表現に求めるのは、1つ以上の単語構成文字を定義し、その後に(任意で)1つ以上の単語構成文字ではない文字が続くパターンです。これに対応する正規表現は以下の通りです。
\w+\W*
バッファの構文テーブルによって、どの文字が単語構成要素であり、どの文字がそうでないかが決まります。構文の詳細については、The GNU Emacs Lisp Reference Manualの構文テーブルを参照してください。
検索式は次のようになります。
(re-search-forward "\\w+\\W*")
(なお、‘w’および‘W’の前には、ペアになったバックスラッシュが置かれています。単一のバックスラッシュは、Emacs Lispインタプリタにおいて特別な意味を持ちます。これは、後続の文字が通常とは異なる方法で解釈されることを示します。例えば、‘\n’という2つの文字は、バックスラッシュに続いて‘n’が来るのではなく、‘newline’を表します。バックスラッシュが2つ連続している場合は、通常の特別な意味を持たないバックスラッシュとなるため、Emacs Lispインタプリタは、単一のバックスラッシュに続いて文字があるのを検知した時点で、その文字が特別なものであることを判断します。)
単語数をカウントするためのカウンタが必要です。この変数はまず0に設定し、Emacsがwhileループを1周するごとにインクリメントする必要があります。インクリメント式は単純に以下の通りです:
(setq count (1+ count))
最後に、領域内に単語がいくつあるかをユーザーに伝えたいと考えます。message
関数は、このような情報をユーザーに提示するためのものです。メッセージは、領域内の単語数に関わらず適切に読めるように表現する必要があります。「領域内に1つの単語があります」とは言いたくないからです。単数形と複数形の混在は文法的に正しくありません。この問題は、領域内の単語数に応じて異なるメッセージを評価する条件式を用いることで解決できます。可能性は、「領域内に単語がない」、「領域内に1つの単語がある」、「2つ以上の単語がある」の3通りです。つまり、cond 特殊形式が適切ということになります。
これらすべてから、次のような関数定義が導かれます。
;;; First version; has bugs!
(defun count-words-example (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. Set up appropriate conditions.
(save-excursion
(goto-char beginning)
(let ((count 0))
;;; 2. Run the while loop. (while (< (point) end) (re-search-forward "\\w+\\W*") (setq count (1+ count)))
;;; 3. Send a message to the user.
(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))))))
記述通り、この関数は動作しますが、あらゆる状況で動作するわけではありません。