除 while 循环外,也可递归编写单词统计函数。下面介绍实现方法。
首先,count-words-example 函数承担三项任务:设置统计所需条件、统计区域内单词、向用户输出统计结果。
若用单个递归函数完成所有工作,每次递归调用都会输出一条信息。若区域有 13 个单词,会连续弹出 13 条信息,这并非我们想要的。因此需拆分为两个函数,其中递归函数嵌套在另一个函数内部:一个负责设置条件并显示信息,另一个返回单词计数。
我们继续将负责显示信息的函数命名为 count-words-example。
该函数由用户直接调用,支持交互,结构与之前版本类似,区别在于调用 recursive-count-words 获取区域单词数。
基于之前版本可快速构建函数模板:
;; 递归版;使用正则表达式搜索
(defun count-words-example (beginning end)
"documentation..."
(interactive-expression...)
;;; 1. 设置合适条件
(explanatory message)
(set-up functions...
;;; 2. 统计单词
recursive call
;;; 3. 向用户输出信息
message providing word count))
定义看似直观,关键在于递归调用返回的计数值需传递给信息展示部分。稍加思考可知,可借助 let 表达式:在 let 变量列表中绑定变量为递归调用返回的单词数,再通过 cond 表达式向用户展示结果。
通常 let 内的绑定被视为函数次要工作,但在此例中,统计单词这一核心工作恰在 let 内部完成。
使用 let 后的函数定义如下:
(defun count-words-example (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))))))
接下来编写递归统计函数。
递归函数至少包含三部分:继续执行条件、步进表达式、递归调用。
继续执行条件决定函数是否再次调用。由于我们在区域内统计单词并可逐词移动光标,该条件可检查光标是否仍在区域内,获取光标位置并判断其在区域结束位置之前、之上还是之后。可使用 point 函数获取光标位置,显然需将区域结束位置作为参数传入递归统计函数。
此外,继续执行条件还需判断搜索是否找到单词,未找到则不再递归调用。
步进表达式用于修改值,使递归函数在合适时机停止调用。更准确地说,它修改值使继续执行条件在恰当时候终止递归。本例中,步进表达式即为逐词推进光标的表达式。
递归函数第三部分是递归调用本身。
同时还需一部分负责实际计数工作,这是核心功能。
至此,递归统计函数的框架已清晰:
(defun recursive-count-words (region-end) "documentation..." do-again-test next-step-expression recursive call)
现在填充具体内容。先处理最简单情况:若光标已达或超出区域末尾,区域内无单词,函数返回 0;同理,若搜索失败,无单词可统计,也返回 0。
反之,若光标在区域内且搜索成功,函数应再次调用自身。
因此,继续执行条件如下
(and (< (point) region-end)
(re-search-forward "\\w+\\W*" region-end t))
注意搜索表达式是继续执行条件的一部分——搜索成功返回 t,失败返回 nil。(See The Whitespace Bug in count-words-example, 查阅 re-search-forward 工作原理。)
继续执行条件作为 if 语句的真假判断。条件成立时,if 真值分支再次调用函数;失败时,假值分支返回 0,表示光标超出区域或搜索未找到单词。
在讨论递归调用前,先明确步进表达式。有趣的是,它就是继续执行条件中的搜索部分。
re-search-forward 除为继续执行条件返回 t 或 nil 外,搜索成功时还会前移光标,这一副作用改变光标位置,使递归函数在光标遍历完区域后停止调用。因此 re-search-forward 表达式即为步进表达式。
综上,recursive-count-words 主体框架如下:
(if do-again-test-and-next-step-combined
;; then
recursive-call-returning-count
;; else
return-zero)
如何融入计数机制?
若不熟悉递归函数编写,这一问题可能令人困惑,但可按逻辑逐步推导。
我们知道,计数机制在某种程度上应当与递归调用相关联。实际上,由于下一步表达式会将指针向前移动一个词,并且对每个词都会执行一次递归调用,因此计数机制必然是这样一种表达式:它会对 recursive-count-words 调用所返回的值加一。
分情况说明:
由框架可知,if 假值分支在无单词时返回 0,因此真值分支必须返回剩余单词计数加 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 分支,函数最终返回零。
如果区域内有一个单词,指针 point 的值小于 region-end 的值,且搜索成功。此时,if 表达式的条件判断结果为真,将执行 if 表达式的 then 分支,并对计数表达式进行求值。该表达式返回的值(也将是整个函数的返回值)为递归调用返回值加一。
与此同时,下一步表达式会使指针 point 跳过区域内的第一个(在此情况下也是唯一的)单词。这意味着,当递归调用第二次对 (recursive-count-words region-end) 求值时,指针 point 的值将等于或大于区域结束位置的值。因此本次 recursive-count-words 会返回零。将零与一相加,最初对 recursive-count-words 的求值将返回一加零,结果为一,即正确的计数。
显然,如果区域内有两个单词,第一次调用 recursive-count-words 会返回一,再加上对包含剩余一个单词的区域调用 recursive-count-words 的返回值 — 也就是一加一,结果为二,即正确的计数。
同理,如果区域内有三个单词,第一次调用 recursive-count-words 会返回一,再加上对包含剩余两个单词的区域调用 recursive-count-words 的返回值,以此类推。
完整文档化后的两个函数如下所示:
递归函数:
(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))))))