精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2007-07-19
统计defun中的单词数量我们的下一个计划是统计函数定义中的单词数量。我们可以使用count-word-region函数的一些变种(正则 表达式方式)来完成这个工作。如果我们只是需要统计定义中的单词数量的话,可以简单的使用C-M-h(mark-defun)命令,然后调用count- word-region。 但我们要进行的是一项雄心勃勃的计划:我们需要统计Emacs源码中所有的函数和符号并打印出各个长度的函数分别有多少个:包含40至49个单词或符号的有多少,包含50到59个单词或符号的有多少,等等。 分割任务这个任务目标使人畏惧;但如果将它分割成多个小的步骤,每次我们只处理其中的一部分,这样这个目标将不那么令人畏惧。先来思考一下有哪些步骤:
统计什么?在上节所说的几个步骤中,首先就是需要决定哪些是需要进行统计的?当我们针对Lisp函数定义说'单词' ('word')时,我们实际上很大程序上是在说'符号'('symbols')。举例来说,multiply-by-seven函数包含了5个符号 defun,multiply-by-seven,number,*,和7。另外,文档字符串包含了四个单词Multiply,Number,by,和 seven。符号number是重复的,因此定义包含了十个单词和符号。 (defun multiply-by-seven (number)但是,如果我们对上面的函数定义执行C-M-h(mark-defun),然后调用count-words-region,count-words-region将报告定义中有11个单词,而不是10。哪里出错了! 原因有两个:count-words-region不把*当作一个单词。把符号multiply-by-seven当作三个单词。连字符被作为单词间的空白。 这是由于count-words-region定义中的正则表达式引起的。在一个典型的count-words-region函数定义中,正则表达式如下: "\\w+\\W*"这个正则表达式匹配一个或多个构词字符被一个或多个非构词字符包围。 单词和符号由什么组成?Emacs把不同的字符归属到不同的语法分类中。比如,正则表达式 语法名指定了字符属于哪个分类。通常,连字符号不被当作构词字符。而是被作为'符号的一部分但不是单词'('class of characters that are part of symbol names but not words.')的一类。这意味着count-words-region函数将把它当作词间的空白一样对侍,这也说明了为什么count-words- region会把multiply-by-seven当作3个单词处理。 有两种办法让Emacs把multiply-by-sevn当作一个符号来处理:修改语法表或修改正则表达式。 我们可以重新在语法表中将连字符定义为构词字符,Emacs将在每个mode中保持这个设置。这个操作能达到我们的目的,除了连字符不是一个典型的构词字符外.. 另外,我们也可以重新定义count-words函数中的正则表达式以包含连字符。这种处理的优点是比较明确,但任务有点刁。 这个正则表达式的第一个部分简单:必须匹配"至少由一个字符或符号构成": "\\(\\w\\|\\s_\\)+" 表达式的第一部分是 表达式的第二个部分更难设计。我们需要在第一个部分后可以有一个非构词字符。首先,我想可以定义成下面的形式: "\\(\\W\\|\\S_\\)*"大写的W和S匹配非构词和非符号字符。 然后我们注意到region中每个单词或符号后面有空白字符(空格、tab、或空行)。因此我们需要让表达式匹配一个或多个构词(或构成符号)字符 后面跟一个或多个空白字符,但实际单词和符号有可能紧跟在括号或标点的后面。最后,我们设计的正则表达式匹配将单词或符号后面跟有可选的非空白字符,然后 跟可选的空白。 完整的表达式如下: "\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*" count-words-in-defun函数前面已经看到过,有多个方法实现count-word-region函数。我们只选用其中一个合适的方式来实现count-words-in-defun。 使用while循环的版本容易理解,因此我们准备采用。因为count-words-in-defun函数将变成更复杂的函数的一部分,它不需要交互也不要显示信息,只需要返回数量。 另外,count-words-in-defun将被用于包含函数定义的缓冲区。因此,需要函数决定当point位于函数定义内部时是否能被调用,如果point位于函数定义内,它需要返回当前所在的函数定义的单词数量。这增加了这个函数的性。 根据上面的需求,我们准备了下面的模板: (defun count-words-in-defun () 与之前一样,我们的工作就是填空。 函数有可能在包含函数定义的缓冲区中。Point有可能位于某个函数定义的内部。count-words-in-defun必须先将point移到这个函数定义的起始位置,计数器置0,计数循环必须在到达函数定义结束位置时停止。 beginning-of-defun函数向后查找左括号。比如行开始位置的 while循环部分需要一个计数器来保存计数。可以使用let语句创建局部变量,并将局部变量初始化为0,来达到这个目的。 end-of-defun函数与beginning-of-defun类似,它将point移到定义的结束位置。end-of-defun可以用于检查是否位于函数定义的结束位置。 count-words-in-defun的开始部分:首先,将point移到定义的开始位置,然后创建一个局部变量保存计数器,最后,记录下定义结束的位置以便while循环知道什么时候停止循环。 代码结构如下: (beginning-of-defun)代码比较简单,唯一复杂点的是"end"部分:它将end设置为save-excursion语句的返回值,这个语句返回end-of-defun(它将point移到定义的结束位置)执行后point的位置。 在初始化工作完成后,count-words-in-defun的第二个部分就是while循环。 这个循环必须包含按单词或符号向前移动的语句,另一个语句则用于统计移动的次数。while循环的true-or-false-test应该跳到定义结束位置时返回false。在这里我们可以使用前面讨论过的正则表达式: (while (and (< (point) end) 函数定义的第三个部分返回符号或单词的数量。这个部分是函数内部的let语句的最后一个表达式。很简单,返回局部变量count。 这几个部分放在一起就构成了count-words-in-defun: (defun count-words-in-defun () 怎样测试它呢?这个函数是非交互式的函数,但我们可以很容易的将它包装成一个交互式的函数;可以使用与count-words-region中类似的方式: ;;; Interactive version. 我们可以将它绑定到 (global-set-key "\C-c=" 'count-words-defun) 现在我们可以试试count-words-defun:安装count-words-in-defun和count-words-defun,设置按键绑定,然后将光标放到下面的定义中: (defun multiply-by-seven (number)将显示: Success! The definition has 10 words and symbols. 下一个问题就是如何统计同一个文件中的多个定义中的单词和符号。 统计一个文件中的多个defun文件simple.el可能包含超过80个函数定义。我们的终极目标是要对很多的文件进行统计,但第一步,我们当前的目标是要对一个文件进行统计。 这个信息将会是一连串的数字,每个数字是一个函数定义的长度。我们可以将这些数字保存到一个list中。 我们需要将多个文件的信息合并到一起,因此统计对一个文件进行统计时不需要显示信息,只需要返回长度信息。 在字数统计命令包含了一个语句用于按单词向前移动另一个语句计数。这个返回函数定义长度的函数同样可以使用这种方式,一个语句用于向前跳转一个函数定义,另一个语句用于计数。 编写函数字义。我们需要从文件开始位置计数,因此第一个命令使用(goto-char (point-min))。接下来,我们开始while循环,循环的true-or-false-test可以是一个查询下一个函数定义的正则表达式查 询,如果查询成功,则将point向前移动,循环体被执行。循环体需要一个语句构造包含长度的list。 代码片段如下: (goto-char (point-min))我们还少了缺少查找函数定义文件的机制。 查找文件在Emacs中可以使用C-x C-f(find-file)命令。这个命令并不是很符合处理当前问题。 先来看find-file的源码(可以使用find-tag命令C-h f(describe-functin)来查找源文件): (defun find-file (filename)定义很短,一个interactive用于执行命令时的交互。定义的body部分包含个函数,find-file-noselect和switch-to-buffer。 使用C-h f(describe-function命令)查看find-file-noselect函数的文档,这个函数读取指定的文件到缓冲区中,并返回这个缓冲 区。但是这个缓冲区未被选中。Emacs并不会将焦点转移到它。这个工作由switch-to-buffer完成,它将Emacs焦点转到指定的缓冲区, 并将这个缓冲区在窗口中显示出来。 在这个工程中,我们并不需要在屏幕上显示每个文件。因此我们使用set-buffer来替代switch-to-buffer,它将程序的焦点转移到另一个缓冲区,但不会改变屏幕显示。因此,我们不调用find-file,而是需要自己编写一个。 可以使用find-file-noselect和set-buffer来完成这个工作。 lengths-list-file函数的细节lengths-list-file函数的核心是一个while循环,它包含了将point向前('defun by defun')移动的函数和用于统计每个defun中符号或单词数量的函数。这个核心将被包含在执行各种任务的函数中,包括文件查找,确保point位于 文件的开始位置。这个函数定义如下: (defun lengths-list-file (filename)这个函数有一个参数,需要处理的文件名。有4行文档字符串,但没有交互式语句。body部分的第一行是一个message,用于提示用户机器正在执行操作。 下一行包括了一个save-excursion它将在函数结束时,将Emacs焦点恢复到当前缓冲区。这通常用于将一个函数嵌入另一个函数时,可以恢复原缓冲区中的point。 在let语句的变量列表中,Emacs打开文件,并将包含该文件缓冲区设置到buffer变量。同时,Emacs创建了局部变量lengths-list。 接下来,Emacs将焦点转到这个缓冲区。 在下一行中,Emacs将缓冲区设置为只读。理想情况下,这行是不必要的。没有哪个计数函数需要修改缓冲区。并且,即使我们修改了缓冲区,缓冲区也不会被保存。这主要是防止不小心修改了Emacs的源码造成麻烦。 接下来,如果缓冲区被narrowed,则调用widen。这个函数在Emacs创建一个新的缓冲区时不需要,但如果文件已经在缓冲区中时,有可能 缓冲区被narrowed了,这时必须调用widen。如果我们要完全的"user-friendly",我们还需要保存point的位置,但我们不需 要。 (goto-char (point-min))语句将point移到缓冲区的开始位置。 后面的while循环中,Emacs决定每个定义的长度并构造一个包含长度信息的列表。 然后,Emacs关闭缓冲区,继续后面的操作。这是为了保存Emacs的空间。在Emacs 19中包含了超过300个源码文件;Emacs 21包含了超过800个源码文件。另一个函数将在每个文件上执行length-list-file。 你可以安装并测试一下这个文件。将光标放在下面的语句的后面,执行C-x C-e(eval-last-sexp): (lengths-list-file 统计不同文件中的defun中的单词前一节,我们创建了一个可以返回单个文件中各个函数长度列表的函数。现在我们需要定一个函数返回文件列表中所有定义长度的函数。 使用while循环或递归在每个文件上执行相同的操作。 决定defun的长度使用while循环作为程序主干。传递给函数的是一个文件列表。前面看过,可以写一个while循环,如果列表中包含 了元素,则执行循环,否则退出循环。循环体必须在每次执行时缩短list的长度,直到list为空退出循环。通常的技巧是将list设置为原来的list 的CDR。 模板如下: (while test-whether-list-is-empty while循环将返回nil(true-or-false-test的返回值),而不是循环体的执行结果。因此我们需要将while循环包含在let语句中,并让let语句的最后一个语句包含要返回的list。 代码如下: ;;; Use while loop. expand-file-name是一个内置函数,它将文件名转换为绝对路径。 如果在debug.el上执行expand-file-nameEmacs将得到 /usr/local/share/emacs/21.0.100/lisp/emacs-lisp/debug.el函数定义的中的另一个新元素是未学习过的函数append。 append函数append函数将一个list添加到另一个list,如下, (append '(1 2 3 4) '(5 6 7 8))将产生list (1 2 3 4 5 6 7 8)这恰好是我们需要结果。如果使用cons, <src lang="lisp"></src> 则,将得到: ((1 2 3 4) 5 6 7 8) 递归统计不同文件中的单词数量除了while循环,你可以在文件列表中使用递归处理。递归版本的lengths-list-many-files简洁一些。 递归函数通常有这些部分:'do-again-test','next-step-expression'和递归调用。'do-again- test'决定是否再次调用自身,它需要检查list-of-files是否还包含有元素;'next-step-expression'将list- of-files重新设置为它的CDR部分,因此,最后这个list将变为空;递归调用则在缩短后的list上调用它自身。代码如下: (defun recursive-lengths-list-many-files (list-of-files)简单来说,函数将第一次返回的list-of-files追加到其它次调用返回的list-of-files中。 这里是一个recursive-lengths-list-many-files的测试。 安装recursive-lengths-list-many-files和lengths-list-file。 (cd "/usr/local/share/emacs/21.0.100/")recursive-lengths-list-many-files函数产生了我们想要的输出。 下一步是准备显示图表的数据。 准备显示图表的数据recursive-lengths-list-many-files函数返回了一个包含计数的列表。每个数字记录了 一个函数定义的长度。我们需要将数据转换到适于生成图表的list中。新的list将告诉我们有多少个定义包含少于10个单词或符号,多少个处于10到 19个单词或符号之间,等等。 我们需要遍历recursive-lengths-list-many-files函数返回的list中的值,并计算处于各个范围中的数量,并产生包含这些数量的list。 基于之前我们所做的,我们可以预想到编写这个函数并不难。可以用截取CDR的方式遍历各个元素,决定这个长度位于哪个范围,并增加这个范围的计数。 但是,在编写这个函数前,我们需要思考对list排序的优点,数字按从小到大的顺序排列。首先,排序将使计数容易一些,因为相信的数字将会处于同一个范围中。第二,检查排序后的list,可以知道最大的数字和最小的数字,便于决定我们所需要的最大和最小的范围。 List排序Emacs包含了一个排序函数sort。sort带两个参数,被排序的list和一个决定list元素大小关系的参数。 sort函数可以基于任意的属性进行排序;这意味着sort可以用于对非数字进行排序,比如按字母。
(sort '(4 8 21 17 33 7 21 7) '<)注意,两个参数前都使用了单引号,表示不需要对它们求值。 也可以使用 (sort(注意,这个例子中第一个参数没加单引号,因为它在传递给sort前需要被执行。) 产生文件列表recursive-lengths-list-many-files函数需要一个文件列表作为参数。在测试的例子 中,我们手工构造了一个文件列表;但Emacs List源码目录太大了。我们需要编写函数来完成这个工作。在这个函数中,我们将要同时使用while循环和递归调用。 在旧版本的GNU Emacs中我们不需要编写这样的函数,因为它将所有的.el文件放在同一个目录中。我们可以使用directory-files函数,它将返回单个目录中匹配指定表达式的文件名的列表。 但是,在新版本的Emacs中Emacs将Lisp文件放到了顶级lisp目录的子目录中。比如所有mail相关的文件放到了mail子目录中。 我们可以创建函数files-in-below-directory,使用car,nthcdr和substring连接已经存在的函数调用 directory-files-and-attributes。这个函数不只是返回目录中的文件名列表,还将返回子目录的名称,和它们的属性。 重新描述我们的目标:创建一个函数能传递下面结构的参数给recursive-lengths-list-many-files函数: ("../lisp/macros.el" directory-files-and-attributes函数返回包含list的list。list中的每个元素是一个包含了13的元素的子 list。第一个元素是包含了文件名,在GNU/Linux中,它可能是一个'directory file',也就是说,它是一个有特殊属性的目录文件。第二个元素为t的表示是一个目录,为字符串时表示是一个符号文件(该字符串表示连接的目标文件), 或者为nil。 比如, 下面是directory-files-and-attributes返回的值: ("/usr/local/share/emacs/21.0.100/lisp/abbrev.el"而表示 mail/ 目录下的mail/ 目录的list如下:
("/usr/local/share/emacs/21.0.100/lisp/mail"(查看file-attributes的文档可以了解这些属性。记住,file-attributes函数不会列出文件名,它的第一个元素是directory-files-and-attributes的第二个元素。) 我们需要让新函数,files-in-below-directory列出目录及其子目录中的 这为我们构造files-in-below-directory给出了提示:在一个目录中,函数需要添加 但是,我们不需要进入表示目录自身的"."目录,也不需要进入上级目录".."。 因此,我们的files-in-below-directory函数必须完成这些任务:
我们将使用while循环在同一个目录中从一个文件移到另一个文件,检查文件是否是需要的;如果是一个子目录则递归调用。递归使用"acumulate"模式,使用append合并结果。 这里是函数定义: (defun files-in-below-directory (directory) files-in-below-directory directory-files函数需要一个参数,目录名称。 在我的系统上, (length我的版本是12.0.100,Lisp源码目录包含754个 .el 文件。
files-in-below-directory返回的list是按字母逆序排列的,可以用一个语句来按字母顺序排列: (sort 统计函数定义的数量我们当前的目标是产生一个list告诉我们有多少个函数定义包含少于10个单词和符号,多少个函数包含10到19个单词和符号,等等。 对于一个排了序的list这很简单:统计list中有多少个元素小于10,然后计算有多少个小于20,如些继续。每个范围的数字,我们可以用一个列表top-of-ranges来定义。 如果需要,我们可以自动生成这个list,手写也比较简单。例如: (defvar top-of-ranges 要修改范围,我们只需要编辑这个list。 接下来我们需要编写函数创建一个包含各个范围数量的列表。这个函数必须传递两个参数,sorted-lengths和top-of-ranges。 defuns-per-range函数必须重复做两件事:它必须统计当前top-of-range值范围内的数字的数量;在一个范围内的数字统计完 成后,它必须移到top-of-ranges的下一个值。由于,每个操作都是重复的,我们可以使用while循环来完成这个工作。一个循环统计一个top -of-ranges中当前范围中定义的数量,另一个循环依次取top-of-range的下一个值。 sorted-lengths列表需要在各个范围内进行多次计数,因此处理sorted-lengths的循环应该在处理top-of-ranges列表的循环的内部。 内部的循环统计一定范围内的数量。可以用一个简单的循环。循环的true-or-false-test检查sorted-lengths列表是否小于top-of-range的当前值。如果是,则函数将计数器加1,然后检查sorted-lengths列表的下一个值。 内部的循环如下: (while length-element-smaller-than-top-of-range 外部的循环从top-of-ranges列表的最小值开始,依次设置为更大的值。循环如下: (while top-of-ranges 两个循环放在一起如下: (while top-of-ranges 另外,每上外部循环,Emacs都要在一个list中记录这个范围内的数量(number-within-range)。我们可以使用cons来达到这个目的。 cons函数工作得很好,它构造的list中,最大范围的将位于开始位置,小范围的位于结束位置。这是因为cons将新元素添加到list的开始位 置,在两个循环中将从小到大的顺序执行,defuns-per-range-list将以最大的数字开始。但我们打印的图表需要以小数字开始。解决的办法 是逆序排列。使用nreverse函数。 举例来说: (nreverse '(1 2 3 4)) 注意,nreverse函数是一个"destructive"(破坏性的函数)类型的函数,它将修改所操作的list;相反的函数是car和cdr 函数,它们是"非破坏性的"。在这里,我们不需要原始的defun-per-range-list,因此不必担心破坏性的问题。(reverse函数担任 了逆序复制list的功能,它也不修改原始的list。) 整个defuns-per-range函数如下: (defun defuns-per-range (sorted-lengths top-of-ranges) 整个函数很直观,除了一个地方。内部循环的true-or-false-test: (and (car sorted-lengths)被替换为了 <src lang="lisp"></src>
简单版本的test在sorted-lengthslist有一个nil值时可以工作。在那种情况下,(car sorted-lengths)将返回nil。而 在统计到list的结束位置时,sorted-lengths列表将变为nil。这样如果使用简单版本的函数在test时也将出错。 解决这个问题的办法就是使用(car sorted-length)语句和and语句。(car sorted-lengths)语句在list中有至少一个值时,会返回一个non-nil值,但如果list为空时将返回nil。and语句先执行 (car sorted-lengths),如果它返回nil,则返回false而不执行 这样,我们避免了一个错误。 这里有一个简短版本的defuns-per-range函数。首先,将top-of-ranges设置为一个list,然后设置sorted-lengths,执行defuns-per-range函数 ;; (Shorter list than we will use later.)返回的list如下: (2 2 2 0 0 1 0 2 0 0 4)实际上,sorted-lengths中有两个元素小于110,两个元素在110和119之间,两个元素在120和129之间,等等。有四个元素大于或等于200。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
浏览 3562 次