count-words-example 的设计 ¶我们先使用 while 循环实现单词统计命令,再改用递归实现。该命令自然需要支持交互调用。
交互式函数定义的模板一如既往:
(defun name-of-function (argument-list) "documentation..." (interactive-expression...) body...)
我们需要填充这些占位部分。
函数名应直观易记。count-words-region 是最直观的选择,但该名称已被 Emacs 标准单词统计命令占用,因此我们将实现命名为 count-words-example。
该函数用于统计区域内单词,因此参数列表需要包含绑定区域起始与结束两个位置的符号,可分别命名为 ‘beginning’ 和 ‘end’。文档字符串首行应为单句,因为类似 apropos 的命令只会显示这部分内容。交互式表达式格式为 ‘(interactive "r")’,使 Emacs 将区域起止位置传入函数参数。这些都是常规写法。
函数主体需要完成三项工作:第一,为 while 循环统计单词设置合适条件;第二,运行 while 循环;第三,向用户输出提示信息。
用户调用 count-words-example 时,光标可能在区域开头或结尾。但统计必须从区域起始位置开始,因此需要用 (goto-char beginning) 确保光标移至起始处。函数执行完毕后,应将光标恢复到原有位置,因此主体需包裹在 save-excursion 表达式中。
函数主体的核心是 while 循环:一个表达式逐词移动光标,另一个表达式统计次数。while 循环的条件判断应在需要继续前进时为真,到达区域末尾时为假。
我们可以用 (forward-word 1) 逐词前移光标,但使用正则表达式搜索能更清晰地看出 Emacs 如何定义 “单词(word)”。
正则表达式搜索在找到匹配模式后,会将光标留在匹配内容的最后一个字符之后。因此连续成功的单词搜索会逐词推进光标。
实际使用中,我们希望正则表达式能跳过单词之间的空白与标点,连同单词一起跳过。若正则表达式无法跳过单词间空白,就永远只能前进一个单词!这意味着正则表达式应包含单词本身以及其后可选的空白与标点。(单词可能位于缓冲区末尾,其后无任何空白或标点,因此这部分必须设为可选。)
因此,我们需要的正则表达式模式为:一个或多个单词构成字符,后接可选的一个或多个非单词构成字符。对应的正则表达式为:
\w+\W*
缓冲区的语法表决定哪些字符属于或不属于单词构成字符。有关语法的更多信息,see Syntax Tables in The GNU Emacs Lisp Reference Manual。
搜索表达式如下:
(re-search-forward "\\w+\\W*")
(注意 ‘w’ 和 ‘W’ 前成对的反斜杠。单个反斜杠对 Emacs Lisp 解释器有特殊含义,表示其后字符按特殊方式解析。例如 ‘\n’ 代表换行而非反斜杠加字母 n。连续两个反斜杠表示普通无特殊含义的反斜杠,因此 Emacs Lisp 最终看到的是单个反斜杠加字母,并识别其特殊含义。)
我们需要一个计数器统计单词数量,该变量初始化为 0,并在每次 while 循环时自增。自增表达式为:
(setq count (1+ count))
最后,我们需要告知用户区域内单词总数。message 函数用于向用户展示此类信息。提示信息的措辞需根据单词数量正确变化,避免出现 “区域内有 1 单词” 这类语法错误。我们可以使用条件表达式,根据区域内单词数量输出不同信息,共三种情况:无单词、一个单词、多个单词。因此使用 cond 特殊形式最为合适。
综合以上内容,得到如下函数定义:
;;; 第一版;存在错误!
(defun count-words-example (beginning end)
"打印区域内单词数量。
单词定义为至少一个单词构成字符,后接至少一个非单词构成字符。
哪些字符属于此类由缓冲区语法表决定。"
(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))))))
当前版本的函数虽可运行,但并非在所有场景下都正常。