设计 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循环的真假测试应该在点应该向前跳转时返回true,在点在区域末尾时返回false。

我们可以使用(forward-word 1)作为将点逐个单词向前移动的表达式,但如果使用正则表达式搜索,可以更容易地看到Emacs将其识别为“单词”的内容。

一个正则表达式搜索找到其正在搜索的模式后,将点留在匹配的最后一个字符之后。这意味着一系列成功的单词搜索将点逐个单词向前移动。

实际上,我们希望正则表达式搜索跳过单词之间的空白符和标点符号,以及单词本身。一个拒绝跳过单词之间空白符的正则表达式搜索永远不会跳过一个以上的单词!这意味着正则表达式应包括单词之后(如果有的话)的空白符和标点符号,以及单词本身。 (一个单词可能结束于缓冲区,而没有任何后续空白符或标点符号,因此该正则表达式的这一部分必须是可选的。)

因此,我们希望正则表达式的模式是定义一个或多个单词成分字符,后面跟着可选的一个或多个不是单词成分字符的字符。这个正则表达式的模式如下:

\w+\W*

缓冲区的语法表决定了哪些字符是单词成分字符,哪些不是。有关语法的更多信息,请参见see Syntax Tables in The GNU Emacs Lisp Reference Manual

搜索表达式看起来像这样:

(re-search-forward "\\w+\\W*")

(请注意,斜杠前面有‘w’和‘W’。单个斜杠对Emacs Lisp解释器有特殊含义。它表示以下字符的解释方式与通常不同。例如,两个字符‘\n’代表‘newline’,而不是反斜杠后跟‘n’。两个连续的斜杠表示一个普通的、非特殊的反斜杠,因此Emacs Lisp解释器最终会看到一个后面跟着一个字母的单个反斜杠。因此,它发现这个字母是特殊的。)

我们需要一个计数器来计算有多少个单词;这个变量必须首先设置为0,然后在Emacs循环while中的每一次都递增。递增表达式很简单:

(setq count (1+ count))

最后,我们想告诉用户区域中有多少个单词。message函数适用于向用户呈现这种类型的信息。消息必须以这样的方式构造,以便不管区域中有多少个单词,它都能正确阅读:我们不想说“区域中有1个单词”。单数和复数之间的冲突是不合语法的。我们可以通过使用条件表达式来解决此问题,该表达式根据区域中的单词数评估不同的消息。有三种可能性:区域中没有单词,区域中有一个单词,以及区域中有多个单词。这意味着cond特殊形式是适当的。

所有这些导致以下函数定义:

;;; 第一个版本;存在错误!
(defun count-words-example (beginning end)
  "打印区域中的单词数。
单词被定义为至少一个单词成分字符,后跟至少一个不是单词成分字符的字符。
缓冲区的语法表决定了这些字符是什么。"
  (interactive "r")
  (message "正在计算区域中的单词数... ")

;;; 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
              "区域中没有单词。"))
            ((= 1 count)
             (message
              "区域中有1个单词。"))
            (t
             (message
              "区域中有%d个单词。" count))))))

按照目前的编写方式,该函数能够工作,但在某些情况下不完全正确。