[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] [?] |
繰り返しと正規表現の探索は、Emacs Lispを書くときによく使う強力な道具である。 本章では、while
ループと再帰を用いた単語を数えるコマンドの 作成をとおして、正規表現の探索の使用例を示す。
Emacsの標準ディストリビューションには、 リージョン内の行数を数える関数が含まれている。 しかし、単語を数える関数はない。
ある種の文書の作成過程では、単語数を知る必要がある。 たとえば、エッセイは800語までとか、 小説を執筆するときには1日に1000語は書くことにするとかである。 Emacsに単語を数えるコマンドがないのは筆者には奇妙に思える。 たぶん、単語数を数える必要のないコードやドキュメントを書くのに Emacsを使っているのであろう。 あるいは、オペレーティングシステムの単語を数えるコマンドwc
を 使っているのであろう。 あるいは、出版社の慣習にしたがって、文書の文字数を5で割って 単語数を計算しているのであろう。
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] [?] |
count-words-region
count-words-region
"へのコメント(無し)
単語を数えるコマンドは、行、段落、リージョン、あるいは、 バッファのなかの単語を数える。 どの範囲で数えるべきであろう? バッファ全体で単語数を数えるようにコマンドを設計することもできるが、 Emacsの習慣では柔軟性を重んじる。 バッファ全体ではなく、ある部分の単語数を数えたい場合もある。 したがって、リージョンの中の単語数を数えるようにコマンドを 設計するほうが合理的であろう。 コマンドcount-words-region
さえあれば、 必要ならば、C-x h(mark-whole-buffer
)で バッファ全体をリージョンとして単語を数えられる。
明らかに、単語を数えるのは繰り返し動作である。 リージョンの先頭から始めて、最初の語を数え、2番目の語を数え、3番目の語を数え というように、リージョンの最後に達するまで繰り返す。 つまり、単語の数え上げは、再帰やwhile
ループに完璧に適しているのである。
まず、while
ループで単語を数えるコマンドを実装してから、 再帰でも書いてみる。 コマンドは、当然、対話的にする。
対話的関数の雛型はつぎのとおりである。
(defun 関数名 (引数リスト) "説明文..." (interactive-expression...) 本体...) |
これらの項目を埋めればよいのである。
関数名は、十分説明的で既存の名前count-lines-region
に 似ているべきである。 こうすると、名前を覚えやすい。 count-words-region
がよいであろう。
関数はリージョン内の単語を数える。 つまり、引数リストには、リージョンの先頭の位置と最後の位置に束縛される シンボルが含まれる必要がある。 これらの2つの位置を、それぞれ、`beginning'、`end'と呼ぶことにする。 apropos
などのコマンドは説明文の最初の1行しか表示しないので、 説明文の最初の1行は1つの文であるべきである。 関数の引数リストにリージョンの先頭と最後を渡す必要があるので、 interactive
式は`(interactive "r")'となる。 これらは、決まりきっていることである。
関数の本体は、3つの仕事を遂行するように書く必要がある。 まず、while
ループが単語を数えられるように条件を設定し、 つぎに、while
ループを実行し、最後に、ユーザーにメッセージを送る。
ユーザーがcount-words-region
を呼ぶときには、 リージョンの先頭か最後のどちらかにポイントがある。 しかし、数え上げの処理はリージョンの先頭から始める必要がある。 つまり、ポイントが先頭になければ移動する必要がある。 (goto-char beginning)
を実行すればよい。 もちろん、関数が終了したらポイントを予想できるような位置に戻したい。 このためには、本体をsave-excursion
式で囲む必要がある。
関数本体の中心部分は、1単語分ポイントを進めて数を数える while
ループから成る。 while
ループの判定条件は、ポイントを移動できる限りは真となり、 ポイントがリージョンの最後に達したら偽となるべきである。
単語単位にポイントを移動する式として(forward-word 1)
を 使うこともできるが、正規表現の検索を使えば Emacsが「単語」と認識するものを容易に理解できる。
正規表現の探索では、探しあてたパターンの最後の文字の直後にポイントを置く。 つまり、単語を正しく連続して探索できるとポイントは単語単位に進むのである。
実際問題として、正規表現の探索では、単語自体だけでなく、 単語と単語のあいだの空白や句読点も飛び越してほしい。 単語と単語のあいだの空白を飛び越せないような正規表現では、 1つの単語を飛び越すこともない。 つまり、正規表現には、単語自体だけでなく、 単語に続く空白や句読点も含める必要がある (単語がバッファの最後で終わっている場合には、空白や句読点が続くことはないので、 これらに対応する正規表現の部分はなくてもよいようになっている必要がある)。
したがって、望みの正規表現は、単語を構成する1個以上の文字のあとに 単語を構成しない文字が0個以上続くようなパターンである。 このような正規表現はつぎのとおりである。
\w+\W* |
どの文字が単語を構成し、どの文字が単語を構成しないかは、 バッファのシンタックステーブル(構文表)で決まる (シンタックスに関して詳しくは、 See 節 14.2 単語やシンボルを構成するものは何か?。 あるいは、節 `The Syntax Table' in
探索式はつぎのようになる。
(re-search-forward "\\w+\\W*") |
(`w'と`W'のまえに、2つの連続したバックスラッシュがあることに 注意してほしい。 単一のバックスラッシュは、Emacs Lispインタープリタに対して特別な意味を持つ。 直後の文字を通常とは異なる意味で解釈することを指示する。 たとえば、2つの文字`\n'は、バックスラッシュに続く`n'ではなく、 `newline'(改行)を意味する。 2つの連続したバックスラッシュは、 普通の「特別な意味のない」バックスラッシュである。)
単語が何個あったかを数えるカウンタが必要である。 この変数は、まず0に設定し、Emacsがwhile
ループを廻るごとに増やす。 増加式は簡単である。
(setq count (1+ count)) |
最後に、リージョン内の単語数をユーザーに伝える必要がある。 関数message
は、この種の情報をユーザーに与えるためのものである。 リージョンの単語数に関わらず、正しいメッセージである必要がある。 「there are 1 words in the region」とは表示したくない。 単数と複数の矛盾は文法的に誤りである。 この問題は、リージョン内の単語数に依存して異なるメッセージを与える条件式を 使えば解決できる。 3つの可能性がある。 リージョンには、0個の単語があるか、1個の単語があるか、 2個以上の単語があるかである。 つまり、スペシャルフォームcond
が適している。
以上により、つぎのような関数定義を得る。
;;; 第1版。バグあり! (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 ループ (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
の空白に関するバグThe Whitespace Bug in count-words-region
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] [?] |
count-words-region
の空白に関するバグcount-words-region
の空白に関するバグ"へのコメント(無し)
上の節で説明したコマンドcount-words-region
には、2つのバグ、 あるいは、2通りの現れ方をする1つのバグがある。 1つめは、文の中程にある空白のみをリージョンとすると、 コマンドcount-words-region
は単語は1個あると報告する! 2つめは、バッファの最後やナロイングしたバッファの参照可能な部分の最後にある 空白のみを含むリージョンでは、 コマンドはつぎのようなエラーメッセージを表示する。
Search failed: "\\w+\\W*" |
GNU EmacsのInfoで読んでいる場合には、これらのバグを読者自身で確認できる。
まず、いつものように関数を評価してインストールする。 Here is a copy of the definition. Place your cursor after the closing parenthesis and type C-x C-e to install it.
;; 第1版。バグあり! (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 ループを実行する (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は、リージョンには単語が3個あると正しく報告する。
行の先頭にマークを、単語`one'の直前にポイントを置いて、 テストを繰り返してみる。 コマンドC-c =(あるいはM-x count-words-region)とタイプする。 行の始めにある空白のみなので、 リージョンには単語がないとEmacsは報告すべきである。 しかし、Emacsはリージョンには単語が1個あると報告する。
3つめのテストとしては、上の例の行をバッファ`*scratch*'の最後にコピーし 行末に空白を数個タイプする。 単語`three'の直後にマークを置き、行末にポイントを置く (行末はバッファの最後でもある)。 まえと同じように、C-c =(あるいはM-x count-words-region) とタイプする。 行末に空白のみがあるので、Emacsはリージョンに単語はないと報告すべきである。 しかし、Emacsは`Search failed'というエラーメッセージを表示する。
2つのバグは同じ問題から生じている。
バグの第一の現れ方では、行の先頭の空白を1語と数える。 これはつぎのことが起きているのである。 コマンドM-x count-words-region
は、 リージョンの先頭にポイントを移動する。 while
では、ポイントの値がend
の値より小さいかどうかを調べるが、 たしかにそのとおりである。 したがって、正規表現の探索が行われ、最初の単語を探す。 それによりポイントは単語の直後に移動する。 count
は1になる。 while
ループが繰り返されるが、ポイントの値がend
の値より 大きいのでループから抜け、 関数はリージョン内に単語が1個ある旨のメッセージを表示する。 つまり、正規表現は単語を探し出すのであるが、 その単語はリージョンの外側にあるのである。
バグの第二の現れ方では、リージョンはバッファの最後の空白である。 Emacsは`Search failed'と報告する。 while
ループの判定条件は真であるため、正規表現の探索が行われる。 しかし、バッファには単語がないので、探索に失敗する。
バグのいずれの現れ方でも、 探索の範囲がリージョンの外側にまでおよんでしまっている。
解決策は、探索をリージョン内に制限することであり、 これは比較的簡単なことであるが、しかし、思ったほどは簡単なことでもない。
すでに説明したように、関数re-search-forward
は、 探索すべきパターンを第1引数に取る。 これは必須引数であるが、これに加えて、3つの引数を取ることができる。 省略できる第2引数は、探索範囲を制限する。 省略できる第3引数にt
を指定すると、探索に失敗した場合には エラーを通知するかわりにnil
を返す。 省略できる第4引数は、繰り返し回数である (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
の第3引数にt
を 指定して、探索に失敗した場合にはエラーを通知するかわりにnil
を 返すようにする。
しかし、この変更を施して試してみると、メッセージ 「Counting words in region ... 」が表示され、 C-g(keyboard-quit
)をタイプするまで、 このメッセージが表示され続ける。
なにが起こっているかというと……。 まえと同じように探索はリージョン内に限られ、 リージョン内には単語を構成する文字がないので探索に失敗する。 その結果、re-search-forward
式はnil
を返す。 それ以外のことはしない。 特に、探索に成功した場合の副作用としてのポイントの移動は行われない。 re-search-forward
式がnil
を返すと、 while
ループのつぎの式が評価される。 この式はカウンタを増やす。 ついでループが繰り返される。 re-search-forward
式はポイントを移動しないので、 ポイントの値は変数end
の値より小さく、判定条件は真になる。 これが繰り返されるのである。
count-words-region
の定義をさらに変更する必要があり、 探索に失敗した場合にはwhile
ループの判定条件が偽になるようにする。 つまり、カウンタを増やすまえに判定条件で満たすべき条件が2つある。 ポイントはリージョン内にあり、かつ、探索式で単語を探し終えていることである。
第一の条件と第二の条件は同時に真である必要があるので、 2つの式、リージョンの検査と探索式を関数and
で結び、 while
ループの判定条件をつぎのようにする。
(and (< (point) end) (re-search-forward "\\w+\\W*" end t)) |
(See 節 12.4 forward-paragraph
:関数の宝庫, for information about and
.)
re-search-forward
式が真を返すのは、 探索に成功し、かつ、副作用としてポイントを移動した場合である。 つまり、単語を探し出すと、リージョン内でポイントが移動する。 別の単語を探すのに失敗したり、リージョンの最後にポイントが達すると、 判定条件は偽になり、while
ループを抜け出し、 関数count-words-region
はメッセージの1つを表示する。
これらの最終的な変更を施すと、count-words-region
はバグなしに (あるいは、少なくとも、筆者にはバグのない)動作をする。 つぎのとおりである。
;;; 最終版
|
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] [?] |
while
ループのかわりに再帰的に単語を数え上げる関数を書くこともできる。 どのようにするかを説明しよう。
まず、関数count-words-region
が3つの処理を行うことを認識する必要がある。 数え上げのための適切な条件を設定する、 リージョン内の単語を数え上げる、 単語の個数をユーザーに伝えるメッセージを送るである。
すべてを行う単一の再帰的関数を書くと、再帰呼び出しごとにメッセージを 受け取ることになる。 たとえば、リージョンに13個の単語があった場合、順番に13個のメッセージを得る。 こうはしたくない。 かわりに、それぞれの処理を行う2つの関数を書き、 一方(再帰的関数)を他方の内部で使う。 一方の関数で条件を設定してメッセージを表示し、他方は数え上げた単語数を返す。
メッセージを表示する関数から始めよう。 これもcount-words-region
と呼ぶことにする。
ユーザーが呼び出すのは、この関数である。 これは対話的にする。 もちろん、これはまえの版の関数に似ているが、 リージョン内の単語を数えるためにrecursive-count-words
を呼び出す。
まえの版をもとにして、この関数の雛型を作ることができる。
;; 再帰版;正規表現の探索を用いる (defun count-words-region (beginning end) "説明文..." (interactive-expression...) ;;; 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)))))) |
つぎは、再帰的に数え上げる関数を書くことである。
再帰関数には少なくとも3つの部分が必要である。 つまり、「再帰条件」、「次段式」、再帰呼び出しである。
再帰条件は、関数が再度呼び出しを行うかどうかを決定する。 リージョン内の単語を数えるのであり、 また、単語ごとにポイントを進めるために関数を 使えるので、再帰条件ではポイントがリージョン内にあるかどうかを調べる。 再帰条件では、ポイントの値を調べて、リージョンの最後のまえか、 最後か、そのうしろかを決定する。 ポイントの位置を知るには関数point
を使う。 明らかに、再帰的に単語を数える関数の引数には、 リージョンの最後を渡す必要がある。
さらに、再帰条件では、単語を探せたかどうかを調べるべきである。 みつからなかった場合には、関数は自身を再度呼び出すべきではない。
次段式は、再帰関数が自身の呼び出しを止めるべきときに止めるように値を変更する。 より正確には、次段式は、再帰条件が再帰関数の自身の呼び出しを止めるように 値を変更する。 この場合、次段式はポイントを1単語分進める式である。
再帰関数の3番目の部分は、再帰呼び出しである。
関数の処理、つまり、数え上げを行う部分も必要である。 これは本質的な部分である!
再帰的に数え上げる関数の概略はすでに説明してある。
(defun recursive-count-words (region-end) "説明分..." 再帰条件 次段式 再帰呼び出し) |
これらの項目を埋めればよい。 もっとも簡単な部分から始めよう。 ポイントがリージョンの最後か最後を越えていれば、 リージョン内に単語があるはずはないので、関数は0を返すべきである。 同様に、探索に失敗した場合にも、 数えるべき単語はないので、関数は0を返すべきである。
一方、ポイントがリージョン内にあり、探索に成功した場合には、 関数は自身を再度呼び出す必要がある。
したがって、再帰条件はつぎのようになる。
(and (< (point) region-end) (re-search-forward "\\w+\\W*" region-end t)) |
探索式は再帰条件の一部であることに注意してほしい。 探索に成功するとt
を返し、失敗するとnil
を返す (re-search-forward
の動作の説明は、 See 節 13.1.1 count-words-region
の空白に関するバグ)。
再帰条件は、if
節の判定条件である。 明らかに、再帰条件が真ならば、if
節の真の場合の動作では関数を呼び出す。 偽ならば、ポイントがリージョンの外側にあるか、 探すべき単語がなくて探索に失敗するので 偽の場合の動作では0を返すべきである。
しかし、再帰呼び出しを考えるまえに、次段式を考える必要がある。 どうすべきだろう? 興味深いことに、次段式は再帰条件の探索部分である。
再帰条件にt
やnil
を返すことに加えて、 re-search-forward
は、探索に成功したときの副作用としてポイントを進める。 この動作は、ポイントがリージョンの最後に達した場合に、 再帰関数が自身の呼び出しを止めるようにポイントの値を変更する。 つまり、re-search-forward
式は次段式でもある。
したがって、関数recursive-count-words
の概略はつぎのようになる。
(if 再帰条件と次段式 ;; 真の場合の動作 個数を返す再帰呼び出し ;; 偽の場合の動作 0を返す) |
数え上げる機構をどのように組み込むか?
再帰関数を書き慣れていないと、このような問いかけは混乱のもとかもしれない。 しかし、系統的に扱う必要があり、また、そうすべきである。
数え上げる機構は、再帰呼び出しに関連付けられるべきであることは知っている。 次段式はポイントを1単語分先へ進め、各単語に対して再帰呼び出しが行われるので、 数え上げる機構は、recursive-count-words
を呼び出して返された値に 1を加える式である必要がある。
いくつかの場合を考えてみよう。
以上のスケッチから、if
の偽の場合の動作では、単語がなければ0を 返すことがわかる。 また、if
の真の場合の動作では、残りの単語を数えて返された値に 1を加えた値を返す必要がある。
引数に1を加える関数を1+
とすれば、式はつぎのようになる。
(1+ (recursive-count-words region-end)) |
recursive-count-words
関数の全体はつぎのようになる。
(defun recursive-count-words (region-end) "説明文..." ;;; 1. 再帰条件 (if (and (< (point) region-end) (re-search-forward "\\w+\\W*" region-end t)) ;;; 2. 真の場合の動作:再帰呼び出し (1+ (recursive-count-words region-end)) ;;; 3. 偽の場合の動作 0)) |
これがどのように動作するか説明しよう。
リージョン内に単語がなければ、if
式の偽の場合の動作が評価され、 その結果、関数は0を返す。
リージョン内に単語が1つある場合、 ポイントの値はregion-end
の値よりも小さく、探索に成功する。 この場合、if
の判定条件は真を返し、真の場合の動作が評価される。 数え上げる式が評価される。 この式は再帰呼び出しが返す値に1を加えた(関数全体の値として返される)値を返す。
同時に、次段式はリージョン内の最初の(この場合は唯一の) 単語を越えてポイントを移動する。 つまり、(recursive-count-words region-end)
が、 再帰呼び出しの結果として2回目に評価されると、 ポイントの値はリージョンの最後に等しいか大きい。 したがって、そのとき、recursive-count-words
は0を返す。 0を1に加えるので、recursive-count-words
のもとの評価値は1足す0、つまり、 1を返し、これは正しい値である。
明らかに、リージョン内に2つの単語がある場合、 recursive-count-words
の始めの呼び出しは、 リージョン内の残りの単語に対して呼び出したrecursive-count-words
が返す値に1を加えた値を返す。 つまり、1足す1は2であり、これは正しい値である。
同様に、リージョン内に3つの単語がある場合には、 recursive-count-words
の最初の呼び出しは、 リージョン内の残りの2つの単語に対して呼び出したrecursive-count-words
が返す値に1を加えた値を返す。
説明文を加えると、2つの関数はつぎのようになる。
再帰関数:
(defun recursive-count-words (region-end) "Number of words between point and REGION-END." ;;; 1. 再帰条件 (if (and (< (point) region-end) (re-search-forward "\\w+\\W*" region-end t)) ;;; 2. 真の場合の動作:再帰呼び出し (1+ (recursive-count-words region-end)) ;;; 3. 偽の場合の動作 0)) |
呼び出し側:
;;; 再帰版 (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)))))) |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] [?] |
while
を使って、リージョン内の句読点、ピリオド、カンマ、セミコロン、 コロン、感嘆符、疑問符を数える関数を書いてみよ。 また、再帰を使って書いてみよ。
[ << ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] [?] |