我们当前的目标是生成一个列表,分别说明:有多少函数定义包含少于 10 个单词与符号,多少个在 10 至 19 个之间,多少个在 20 至 29 个之间,依此类推。
有了排序后的数字列表,统计就很简单:先统计列表中小于 10 的元素个数,跳过已统计数字后再统计小于 20 的个数,接着跳过再统计小于 30 的个数,以此类推。10、20、30、40 等数字均为对应区间上限加一,我们将这类数字组成的列表称为 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) "List specifying ranges for `defuns-per-range'.")
如需修改区间,直接编辑该列表即可。
接下来需要编写函数,生成每个区间内的函数定义数量列表。显然该函数需要接收 sorted-lengths 与 top-of-ranges 两个列表作为参数。
defuns-per-range 函数需要重复执行两件事:统计当前区间上限对应的函数定义数量;统计完成后切换到 top-of-ranges 中的下一个更大值。由于这些操作都是重复的,我们可以使用 while 循环实现:一个循环统计当前区间上限对应的函数数量,另一个循环依次选取每个区间上限值。
每个区间会统计 sorted-lengths 中的多项内容,因此 sorted-lengths 的循环应嵌套在 top-of-ranges 的循环内部,如同小齿轮在大齿轮内部。
内层循环统计区间内的函数数量,是我们之前见过的简单计数循环。(See A loop with an incrementing counter。)循环的真假判断用于检查 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 ;; Count the number of elements within the current range. (while length-element-smaller-than-top-of-range (setq number-within-range (1+ number-within-range)) (setq sorted-lengths (cdr sorted-lengths))) ;; Move to next range. (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)
"SORTED-LENGTHS defuns in each TOP-OF-RANGES range."
(let ((top-of-range (car top-of-ranges))
(number-within-range 0)
defuns-per-range-list)
;; Outer loop.
(while top-of-ranges
;; Inner loop. (while (and ;; Need number for numeric test. (car sorted-lengths) (< (car sorted-lengths) top-of-range))
;; Count number of definitions within current range. (setq number-within-range (1+ number-within-range)) (setq sorted-lengths (cdr sorted-lengths))) ;; Exit inner loop but remain within outer loop.
(setq defuns-per-range-list
(cons number-within-range defuns-per-range-list))
(setq number-within-range 0) ; Reset count to zero.
;; Move to next range. (setq top-of-ranges (cdr top-of-ranges)) ;; Specify next top of range value. (setq top-of-range (car top-of-ranges)))
;; Exit outer loop and count the number of defuns larger than ;; the largest top-of-range value. (setq defuns-per-range-list (cons (length sorted-lengths) defuns-per-range-list))
;; Return a list of the number of definitions within each range, ;; smallest to largest. (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 不为空时正常,但当列表为空时,(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 则直接返回假,不会再执行 < 表达式;若为非 nil 值,则继续执行 < 并将结果作为 and 的返回值。
这样就避免了报错。
下面是对 defuns-per-range 函数的简短测试。先对绑定(简化版)top-of-ranges 列表的表达式求值,再对绑定 sorted-lengths 列表的表达式求值,最后对 defuns-per-range 求值。
;; (Shorter list than we will use later.)
(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 中有 2 个元素小于 110,2 个在 110 至 119 之间,2 个在 120 至 129 之间,依此类推,有 4 个元素大于等于 200。