我们的即时目标是生成一个列表,告诉我们有多少个函数定义包含少于10个字和符号,有多少包含10到19个字和符号,有多少包含20到29个字和符号,依此类推。
有了一个排序过的数字列表,这很容易实现:统计列表中有多少元素小于10,然后在移过刚刚计数的数字后,统计列表中有多少元素小于20,然后在移过刚刚计数的数字后,统计列表中有多少元素小于30,依此类推。每个数字,10、20、30、40以及类似的,都比该范围的顶部大1。我们可以称这个数字列表为top-of-ranges
列表。
如果愿意,我们可以自动生成这个列表,但手动编写列表更简单。下面是一个例子:
(defvar top-of-ranges '(10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 210 220 230 240 250 260 270 280 290 300) "指定 `defuns-per-range' 的范围的列表。")
要更改范围,我们编辑此列表。
接下来,我们需要编写一个函数,该函数创建包含在每个范围内的定义数量列表。显然,此函数必须将sorted-lengths
和top-of-ranges
列表作为参数。
defuns-per-range
函数必须反复执行两件事:它必须计算在当前顶部范围值指定的范围内的定义数量;并且在计算当前范围内的定义数量后,它必须转移到top-of-ranges
列表中的下一个更高的值。由于每个动作都是重复的,我们可以使用while
循环完成这项工作。一个循环计算当前范围内的定义数量,另一个循环依次选择top-of-ranges
中的每个顶部范围值。
sorted-lengths
列表的每个条目都会计数多次,这意味着sorted-lengths
列表的循环将在top-of-ranges
列表的循环内部,就像小齿轮在大齿轮内部一样。
内部循环计算范围内的定义数量。它是我们之前见过的简单计数循环的类型。(See 具有增量计数器的循环.) 循环的真假测试测试sorted-lengths
列表中的值是否小于范围顶部的当前值。如果是,则函数增加计数器并测试sorted-lengths
列表的下一个值。
内部循环如下:
(while length-element-smaller-than-top-of-range (setq number-within-range (1+ number-within-range)) (setq sorted-lengths (cdr sorted-lengths)))
外部循环必须从top-of-ranges
列表的最小值开始,然后依次设置为每个后续更高的值。可以使用如下循环实现:
(while top-of-ranges body-of-loop… (setq top-of-ranges (cdr top-of-ranges)))
组合在一起,两个循环如下:
(while top-of-ranges ;; 计算当前范围内的元素数量。 (while length-element-smaller-than-top-of-range (setq number-within-range (1+ number-within-range)) (setq sorted-lengths (cdr sorted-lengths))) ;; 移到下一个范围。 (setq top-of-ranges (cdr top-of-ranges)))
此外,在外部循环的每一次迭代中,Emacs应该记录该范围内的定义数量(number-within-range
的值)到一个列表中。我们可以使用cons
来完成这个目的。(See cons
.)
cons
函数运行正常,但它构造的列表将以最大范围的定义数量开头,并以最小范围的定义数量结尾。这是因为cons
将新元素附加到列表的开头,而由于两个循环从较低端开始遍历长度的列表,因此defuns-per-range-list
将以最大的数字开始。但我们希望以最小值优先打印我们的图形,然后再打印较大的值。解决方案是颠倒defuns-per-range-list
的顺序。我们可以使用nreverse
函数实现这一点,该函数颠倒列表的顺序。
例如,
(nreverse '(1 2 3 4))
产生:
(4 3 2 1)
请注意,nreverse
函数是破坏性的—也就是说,它改变了其应用的列表;这与car
和cdr
函数不同,它们是非破坏性的。在这种情况下,我们不需要原始的defuns-per-range-list
,因此它被销毁并不重要。(reverse
函数提供列表的反向副本,保留原始列表不变。)
全部组合在一起,defuns-per-range
如下:
(defun defuns-per-range (sorted-lengths top-of-ranges) "在每个TOP-OF-RANGES范围内,对SORTED-LENGTHS进行函数定义。" (let ((top-of-range (car top-of-ranges)) (number-within-range 0) defuns-per-range-list)
;; 外部循环。
(while top-of-ranges
;; 内部循环。 (while (and ;; 需要用于数字测试的数字。 (car sorted-lengths) (< (car sorted-lengths) top-of-range))
;; 计算当前范围内的定义数量。 (setq number-within-range (1+ number-within-range)) (setq sorted-lengths (cdr sorted-lengths))) ;; 退出内部循环但仍保持在外部循环中。
(setq defuns-per-range-list
(cons number-within-range defuns-per-range-list))
(setq number-within-range 0) ; 将计数器重置为零。
;; 移至下一个范围。 (setq top-of-ranges (cdr top-of-ranges)) ;; 指定下一个范围的顶部值。 (setq top-of-range (car top-of-ranges)))
;; 退出外部循环并计算大于 ;; 最大顶部范围值的函数数量。 (setq defuns-per-range-list (cons (length sorted-lengths) defuns-per-range-list))
;; 返回每个范围内定义数量的列表, ;; 从最小到最大。 (nreverse defuns-per-range-list)))
该函数很直观,除了一个微妙的特征。内部循环的真假测试看起来像这样:
(and (car sorted-lengths) (< (car sorted-lengths) top-of-range))
而不是像这样:
(< (car sorted-lengths) top-of-range)
该测试的目的是确定sorted-lengths
列表中的第一个项是否小于范围顶部的值。
简单版本的测试很好,除非sorted-lengths
列表有一个nil
值。在这种情况下,(car sorted-lengths)
表达式函数返回nil
。<
函数无法将数字与nil
(一个空列表)进行比较,因此Emacs会发出错误并阻止函数继续执行。
当计数器达到列表末尾时,sorted-lengths
列表始终变为nil
。这意味着任何尝试使用简单版本测试的defuns-per-range
函数都将失败。
我们通过使用(car sorted-lengths)
表达式与and
表达式结合来解决问题。(car sorted-lengths)
表达式只要列表中至少有一个数字,就会返回非nil
值,但如果列表为空,则返回nil
。and
表达式首先评估(car sorted-lengths)
表达式,如果它是nil
,则不评估<
表达式,直接返回false。但如果(car sorted-lengths)
表达式返回非nil
值,则and
表达式评估<
表达式,并将该值作为and
表达式的值返回。
这样,我们避免了错误。
以下是对defuns-per-range
函数的简短测试。首先,评估将(缩短的)top-of-ranges
列表绑定到值列表的表达式,然后评估绑定sorted-lengths
列表的表达式,最后评估defuns-per-range
函数。
;; (比我们稍后将使用的列表更短.)
(setq top-of-ranges
'(110 120 130 140 150
160 170 180 190 200))
(setq sorted-lengths
'(85 86 110 116 122 129 154 176 179 200 265 300 300))
(defuns-per-range sorted-lengths top-of-ranges)
返回的列表如下:
(2 2 2 0 0 1 0 2 0 0 4)
确实,sorted-lengths
列表中有两个小于110的元素,两个元素介于 110 和 119 之间,两个元素介于 120 和 129 之间,依此类推。有四个值等于或大于 200 的元素。