単語数を数える関数は、再帰的に記述することも、whileループを用いて記述することもできます。それでは、その方法を見てみましょう。
まず、count-words-example関数には3つの役割があることを認識しておく必要があります。それは、カウントが行われるための適切な条件を整えること、領域内の単語数を数えること、そして単語数をユーザーに通知するメッセージを送信することです。
すべてを処理する再帰関数を1つだけ書くと、再帰呼び出しのたびにメッセージが出力されます。領域に13個の単語がある場合、13通のメッセージが立て続けに出力されることになります。これは避けたいところです。代わりに、処理を行うために2つの関数を記述し、そのうちの1つ(再帰関数)をもう一方の中で使用するようにします。一方の関数が条件を設定してメッセージを表示し、もう一方が単語数を返します。
メッセージを表示させる関数から始めましょう。これを引き続き count-words-example
と呼ぶことにします。
これはユーザーが呼び出す関数です。対話型となります。実際、この関数の以前のバージョンと同様ですが、領域内の単語数を判定するために recursive-count-words を呼び出す点が異なります。
これまでのバージョンを基に、この関数のテンプレートを容易に作成できます:
;; Recursive version; uses regular expression search
(defun count-words-example (beginning end)
"documentation..."
(interactive-expression...)
;;; 1. Set up appropriate conditions.
(explanatory message)
(set-up functions...
;;; 2. Count the words.
recursive call
;;; 3. Send a message to the user.
message providing word count))
定義は一見単純に見えますが、どういうわけか、再帰呼び出しによって返されたカウント値を単語数を表示するメッセージに渡す必要があります。少し考えてみると、これはlet式を利用することで実現できることがわかります。具体的には、let式のvarlistにある変数に、再帰呼び出しで返された領域内の単語数をバインドし、その後、cond式がそのバインドを用いてその値をユーザーに表示すればよいのです。
多くの場合、let
式内の束縛は、関数の主たる役割に比べれば二次的なものだと考えられがちです。しかしこの場合、単語数を数えるという、関数の主たる役割とも呼べる処理は、let 式の中で行われています。
letを使うと、関数定義は次のようになります。
(defun count-words-example (beginning end) "Print number of words in the region." (interactive "r")
;;; 1. Set up appropriate conditions.
(message "Counting words in region ... ")
(save-excursion
(goto-char beginning)
;;; 2. Count the words.
(let ((count (recursive-count-words end)))
;;; 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))))))
次に、再帰的なカウント関数を記述する必要があります。
再帰関数は、少なくとも「再実行判定」、「次のステップの式」、「再帰呼び出し」の3つの部分から構成される。
do-again-testは、関数が再度呼び出されるかどうかを判定します。ある領域内の単語数をカウントしており、単語ごとにポイントを前方に進める関数を利用できるため、do-again-testでポイントがまだ領域内にあるかを確認できます。do-again-testではポイントの値を取得し、ポイントが領域の終端の値より前か、等しいか、あるいは後かを判定する必要があります。ポイントの位置を特定するには、point関数を利用できます。当然ながら、領域の終端の値を引数として再帰的なカウント関数に渡す必要があります。
さらに、do-again-testでは、検索で単語が見つかったかどうかも判定する必要があります。見つからなかった場合、関数は自身を再呼び出しすべきではありません。
次ステップ式は、再帰関数が自身を呼び出すのを止めるべきタイミングで、実際に停止するように値を変更します。より正確には、次ステップ式は、適切なタイミングで do-again-test が再帰関数の再呼び出しを停止させるように値を変更します。この場合、次ステップ式は、ポイントを単語ごとに前方へ進める式であっても構いません。
再帰関数の3番目の部分は、再帰呼び出しです。
どこかに、関数の役割を果たすパーツ、つまりカウントを行うパーツも必要だ。不可欠なパーツだ!
しかし、すでに再帰的な計数関数の概要は出来上がっています:
(defun recursive-count-words (region-end) "documentation..." do-again-test next-step-expression recursive call)
次に、空欄を埋めていきましょう。まずは最も単純なケースから考えます。点が領域の端にあるか、あるいはそれを超えている場合、その領域内に単語は存在し得ないため、関数は0を返すべきです。同様に、検索に失敗した場合も、カウントすべき単語は存在しないため、関数は0を返すべきです。
一方、点が領域内にあり、探索が成功した場合は、関数は自身を再呼び出しする必要があります。
したがって、do-again-testは次のようになります。
(and (< (point) region-end)
(re-search-forward "\\w+\\W*" region-end t))
なお、検索式は do-again-test の一部であり、関数は検索に成功した場合は t
を、失敗した場合は nil を返します。(re-search-forward の動作に関する説明については、count-words-example
におけるホワイトスペース・バグ を参照してください。)
do-again-testは、if節の真偽判定テストである。明らかに、do-again-testが成功すれば、if節のthen部は関数を再度呼び出すべきである。一方、失敗した場合は、いずれかの地点が領域外であるか、あるいは検索すべき単語が存在せず検索が失敗したため、else部は0を返すべきである。
しかし、再帰呼び出しを検討する前に、次のステップの式を考える必要があります。それは一体何でしょうか?興味深いことに、それはdo-again-testの探索部分なのです。
do-again-test に対して t または nil を返すほか、re-search-forward
は探索成功の副作用として point を前方に移動させます。これは、point
が領域内の移動を完了した際に再帰関数が自身を呼び出すのを停止するよう、point の値を変更する処理です。したがって、re-search-forward 式が次のステップ式となります。
まとめると、recursive-count-words関数の本体は次のようになります。
(if do-again-test-and-next-step-combined
;; then
recursive-call-returning-count
;; else
return-zero)
カウントする仕組みをどう組み込むか?
再帰関数を書くことに慣れていないと、このような問題は厄介かもしれません。しかし、体系的にアプローチすることは可能ですし、そうすべきです。
カウント機構は、何らかの形で再帰呼び出しと関連している必要がある。実際、次ステップ式はポインタを1単語分進め、また単語ごとに再帰呼び出しが行われるため、カウント機構はrecursive-count-wordsへの呼び出しによって返される値に1を加算する式でなければならない。
いくつかのケースを考えてみましょう:
スケッチから、if の else 部分は単語がない場合に 0
を返すことがわかります。つまり、if の then 部分は、残りの単語数のカウントから得られた値に
1 を加算した結果を返す必要があります。
式は次のようになります。ここで 1+ は、引数に 1 を加える関数です。
(1+ (recursive-count-words region-end))
すると、recursive-count-words 関数全体は次のようになります。
(defun recursive-count-words (region-end)
"documentation..."
;;; 1. do-again-test
(if (and (< (point) region-end)
(re-search-forward "\\w+\\W*" region-end t))
;;; 2. then-part: the recursive call (1+ (recursive-count-words region-end)) ;;; 3. else-part 0))
これがどのような仕組みなのか、見てみましょう:
その領域に単語が存在しない場合、if 式の else 部分が評価され、その結果、関数は 0
を返します。
その領域に単語が1つしかない場合、pointの値はregion-endの値よりも小さく、探索は成功します。この場合、if式の真偽判定はtrueとなり、if式のthen部分が評価されます。カウント式が評価されます。この式は、再帰呼び出しによって返された値に1を加えた和(これが関数全体によって返される値となります)を返します。
一方、next-step-expression によって、point は region
内の最初の(そしてこの場合唯一の)単語を飛び越えました。つまり、再帰呼び出しの結果として (recursive-count-words region-end) が2回目に評価される際、point の値は
region end の値以上になります。したがって、今回は 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. do-again-test
(if (and (< (point) region-end)
(re-search-forward "\\w+\\W*" region-end t))
;;; 2. then-part: the recursive call (1+ (recursive-count-words region-end)) ;;; 3. else-part 0))
ラッパー:
;;; Recursive version
(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 ... ")
(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))))))