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))))))

当前版本的函数虽可运行,但并非在所有场景下都正常。