【Elisp官方指南,翻译】3 如何编写函数定义
【Elisp官方指南,翻译】3 如何编写函数定义
原文地址:https://www.gnu.org/software/emacs/manual/html_node/eintr/Writing-Defuns.html
备注:使用通义千问进行翻译,略微修正。
3 如何编写函数定义
在Lisp解释器评估一个列表时,它会查看列表中首项符号是否关联有函数定义;也就是说,该符号是否指向一个函数定义。如果确实存在这样的函数定义,计算机将执行该定义中的指令。这样拥有函数定义的符号,通常被称为函数(尽管从严格意义上来讲,函数定义本身才是函数,而符号只是指向它的指针。)
关于原始函数的旁注
并非所有函数都是基于其他函数定义的,除了少数由C语言编写的原始函数。当你编写函数定义时,你将在Emacs Lisp中编写,并以其他函数作为构建基础。你所使用的部分函数可能是用Emacs Lisp编写的(可能由你自己编写),而另一些则是用C语言编写的原始函数。原始函数与用Emacs Lisp编写的函数使用方式完全相同,并且表现相似。它们之所以用C语言编写,是为了方便我们在任何具有足够性能并能够运行C语言的计算机上轻松运行GNU Emacs。
我再次强调这一点:当你在Emacs Lisp中编写代码时,你无需区分使用的是用C语言编写的函数还是用Emacs Lisp编写的函数。这种区别是无关紧要的。我提到这种区分仅仅是因为了解这一点很有趣。事实上,除非你去探究,否则你不会知道一个已编写好的函数究竟是用Emacs Lisp还是C语言编写的。
defun宏
在Lisp中,诸如mark-whole-buffer这样的符号会附带一段代码,告诉计算机在调用该函数时应执行什么操作。这段代码被称为函数定义,是通过评估以defun(“define function”的缩写)符号开头的Lisp表达式创建的。
在后续章节中,我们将研究来自Emacs源代码的函数定义,例如mark-whole-buffer。在本节中,我们将描述一个简单的函数定义,以便您了解其外观。这个函数定义使用了算术运算,因为它是一个简单的示例。有些人不喜欢使用算术的例子;然而,如果你是这类人,请不要灰心。在本介绍剩余部分我们将要学习的几乎全部代码都不涉及算术或数学,大部分例子都以某种方式与文本处理相关。
函数定义通常包含以下五个部分,紧跟在defun关键字之后:
- 函数定义应附加到的符号名称。
- 将传递给函数的参数列表。如果函数不需要参数,则此列表为空列表,即(()).
- 描述函数功能的文档。(从技术上讲可选,但强烈建议提供。)
- 可选地,用于使函数交互式的表达式,以便可以通过键入M-x和函数名来调用函数;或者通过键入适当的键或组合键来调用。
- 指示计算机应执行的操作的代码:函数定义的主体。
可以将函数定义的这五个部分视为组织在一个模板中的,每个部分都有对应的插槽:
(defun 函数名 (参数...)
"可选文档说明..."
(interactive 参数传递信息) ; 可选
函数主体...)
作为示例,以下是计算其参数乘以7的函数的代码。(这个例子是非交互式的。关于交互式信息,请参阅“使函数交互式”部分。)
(defun multiply-by-seven (number)
"将NUMBER乘以七。"
(* 7 number))
这个定义以左括号和符号defun开始,后面跟着函数的名称。
函数名称后面跟着一个列表,其中包含将传递给函数的参数。这个列表称为参数列表。在这个例子中,列表只有一个元素,即符号number。当函数被使用时,该符号将绑定到作为函数参数使用的值。
对于参数名称的选择,我可以选择任意其他名称代替“number”。例如,我可以选择“multiplicand”这个词。我之所以选择“number”,是因为它表明了这个位置所期望的值类型;但我同样可以选用“multiplicand”这个词来表示放置在此位置的值在函数运行过程中扮演的角色。我甚至可以将其命名为foogle,但这将是一个糟糕的选择,因为它不会告诉人们它的含义。参数名称的选择取决于程序员,应选择能清楚表达函数意义的名称。
实际上,你可以在参数列表中选择任何你想要的符号名称,甚至是其他函数中已使用的符号名称:参数列表中的名称对该特定定义而言是私有的。在该定义中,名称所指的实体与函数定义外部相同名称的使用不同。例如,你在家庭中有昵称“Shorty”,当你家人提到“Shorty”时,他们指的是你。但在家庭之外,在电影中,“Shorty”可能指的是另一个人。由于参数列表中的名称对函数定义而言是私有的,因此你可以在函数体内更改这种符号的值,而不会影响函数外部该符号的值。这种效果类似于let表达式产生的效果。(参见let。)
参数列表之后是描述函数的文档字符串。当你键入C-h f和函数名时,可以看到这个文档字符串。顺便提一下,当你编写这样的文档字符串时,应该确保第一行是一个完整的句子,因为某些命令(如apropos)只打印多行文档字符串的第一行。另外,如果有第二行文档字符串,你不应该对其缩进,因为在使用C-h f(describe-function)时,这样做看起来会很奇怪。文档字符串是可选的,但由于它非常有用,所以几乎在你编写的每一个函数中都应该包含它。
示例的第三行是函数定义的主体。(当然,大多数函数的定义都会比这个更长。)在这个函数中,主体是列表(* 7 number),表示将number的值乘以7。(在Emacs Lisp中,* 是用于乘法的函数,正如+ 是用于加法的函数一样。)
当你使用multiply-by-seven函数时,参数number会求值为你实际想要使用的数字。下面是一个展示如何使用multiply-by-seven函数的例子,但现在还不要尝试求值!
(multiply-by-seven 3)
在函数定义中指定的符号number,在实际使用函数时会被绑定到值3。请注意,虽然在函数定义中number位于括号内,但在传递给multiply-by-seven函数的参数并未包含在括号内。在函数定义中写入括号是为了让计算机能够识别参数列表的结束以及函数定义其余部分的开始。
如果你尝试求值这个例子,可能会收到错误消息。(尽管去试试吧!)这是因为我们已经编写了函数定义,但还没有告诉计算机有关该定义的信息——我们还没有在Emacs中加载函数定义。安装函数是指告知Lisp解释器函数定义的过程。安装函数将在下一节中进行描述。
3.2 安装函数定义
在Emacs的Info中阅读本文档时,你可以通过首先评估函数定义,然后评估(multiply-by-seven 3)来尝试使用multiply-by-seven函数。以下是该函数定义的副本。将光标置于函数定义最后一个括号之后,然后按C-x C-e键。当你执行此操作后,“multiply-by-seven”将会出现在回显区。(这意味着当一个函数定义被求值时,其返回的值是所定义函数的名称。)同时,这个动作会安装函数定义。
(defun multiply-by-seven (number)
"将NUMBER乘以七。"
(* 7 number))
通过评估这个defun表达式,你刚刚将multiply-by-seven函数安装到了Emacs中。现在,这个函数已经与forward-word或其他任何你使用的编辑功能一样,成为了Emacs不可或缺的一部分。(multiply-by-seven函数将持续安装直到你退出Emacs。如果你想在每次启动Emacs时自动重新加载代码,请参阅“永久性安装代码”。)
安装效果的体现
你可以通过评估以下示例来观察安装multiply-by-seven函数的效果。将光标置于下面表达式之后,然后按C-x C-e键,数字21将会出现在回显区。
(multiply-by-seven 3)
如果你想查看该函数的文档,可以键入C-h f(describe-function),接着输入函数名multiply-by-seven。当你这样做时,屏幕上会出现一个帮助窗口,内容如下:
multiply-by-seven 是一个 Lisp 函数。
(multiply-by-seven NUMBER)
将 NUMBER 乘以七。
(若要回到单窗口显示状态,在屏幕上键入C-x 1即可。)
3.2.1 更改函数定义
如果你想更改multiply-by-seven中的代码,只需重写它。为了用新版本替换旧版本,在Emacs中只需再次评估该函数定义即可。这就是在Emacs中修改代码的方法,非常简单。
举个例子,你可以将multiply-by-seven函数更改为将数字与其自身相加七次,而不是将数字乘以七。这样虽然通过不同的路径实现,但产生的结果是相同的。同时,我们会在代码中添加一个注释;注释是Lisp解释器忽略的文本,但对于人类读者来说可能有用或有启发意义。这个注释表明这是第二个版本。
(defun multiply-by-seven (number) ; 第二版。
"Multiply NUMBER by seven."
(+ number number number number number number number))
注释紧跟在分号(;)之后。在Lisp中,一行中分号后面的所有内容都是注释。行尾即注释的结束。如果需要跨越两行或多行的注释,则应在每一行开始处都放置一个分号。
有关更多关于注释的信息,请参阅《如何创建.emacs文件》以及《GNU Emacs Lisp参考手册》中的“注释”部分。
你可以通过与安装第一个函数相同的方式安装这个版本的multiply-by-seven函数:将光标置于最后一个括号后,然后键入C-x C-e。
总结一下,在Emacs Lisp中编写代码的过程如下:你编写一个函数;安装它;测试它;然后进行修复或增强,并再次安装它。
3.3 使函数交互式
要使一个函数交互式,你需要在文档之后立即放置一个以特殊形式interactive开始的列表。用户可以通过键入M-x然后输入函数名的方式来调用交互式函数;或者通过键入该函数绑定的按键来调用,例如,键入C-n调用next-line函数或键入C-x h调用mark-whole-buffer函数。
有趣的是,当你以交互方式调用一个交互式函数时,返回的值并不会自动显示在回显区中。这是因为你经常调用交互式函数是为了实现其副作用,比如向前移动一个词或一行,而不是为了获取返回的值。如果每次按下按键时都显示返回值,将会非常分散注意力。
交互式multiply-by-seven的概述
通过创建一个交互式的multiply-by-seven版本,我们可以直观地展示特殊形式interactive的使用以及在回显区显示值的一种方法。
以下是相关代码:
(defun multiply-by-seven (number) ; 交互式版本。
"将NUMBER乘以七。"
(interactive "p")
(message "结果是 %d" (* 7 number)))
你可以通过将光标放置在该代码之后并键入C-x C-e来安装这段代码。函数名称将会出现在你的回显区域中。然后,你可以通过键入C-u和一个数字,接着键入M-x multiply-by-seven并按RET键来使用这段代码。在回显区域中会显示出“结果是…”后面跟着计算结果的数值。
更广泛地说,调用函数可以通过以下两种方式实现:
- 先键入包含要传递给函数的数值的前缀参数,再键入M-x和函数名,例如C-u 3 M-x forward-sentence;
- 直接键入函数所绑定到的键或组合键,例如C-u 3 M-e。
上述提到的两个例子都能够同样地将光标向前移动三个句子的位置。(由于multiply-by-seven并未绑定到任何键上,因此无法作为键绑定的例子来演示。)
(参阅“一些键绑定”章节,了解如何将命令绑定到某个键上。)
向交互式函数传递前缀参数可通过先按下META键再输入一个数字的方式实现,例如M-3 M-e;或者通过键入C-u后跟一个数字,例如C-u 3 M-e(如果你仅键入C-u而不跟随数字,默认为4)。
3.3.1 交互式multiply-by-seven详解
让我们仔细研究一下特殊形式interactive在交互式版本的multiply-by-seven函数中的使用,以及message函数的作用。您会记得该函数定义如下:
(defun multiply-by-seven (number) ; 交互式版本。
"将NUMBER乘以七。"
(interactive "p")
(message "结果是 %d" (* 7 number)))
在这个函数中,表达式(interactive "p")是一个包含两个元素的列表。“p”告诉Emacs将前缀参数传递给函数,并将其值作为函数的参数使用。
参数将会是一个数字。这意味着在以下行中符号number将被绑定到一个数字上:
(message "结果是 %d" (* 7 number))
例如,如果你的前缀参数为5,Lisp解释器将会像处理下面的表达式一样来评估这一行:
(message "结果是 %d" (* 7 5))
(如果你正在GNU Emacs中阅读本文档,可以自己尝试评估这个表达式。)首先,解释器会评估内部列表,即(* 7 5),返回结果为35。接下来,它会评估外部列表,将列表第二个及后续元素的值传给函数message。
正如我们所见,message是Emacs Lisp中专门设计用于向用户发送单行消息的函数。(参阅“message函数”部分。)总结来说,message函数会打印其第一个参数的内容至回显区,但遇到'%d'或'%s'(以及其他未提及的各种%序列)时会进行特殊处理。当它检测到控制序列时,函数会查找第二个或后续参数,并将参数值打印在字符串中控制序列所在的位置。
在交互式multiply-by-seven函数中,控制字符串是'%d',需要一个数字,而计算(* 7 5)的结果正是数字35。因此,在'%d'的位置会打印出数字35,最终的消息内容为“结果是35”。
(请注意,当你调用函数multiply-by-seven时,消息打印时不带引号;而当你直接调用message函数时,文本会被双引号包围打印。这是因为message函数的返回值会在你评估首个元素为message的表达式时出现在回显区;但在嵌入到函数中时,message函数会作为一个副作用无引号地打印文本内容。)
3.4 interactive的不同选项
在示例中,multiply-by-seven使用了"p"作为interactive的参数。这个参数告诉Emacs将你键入的C-u后跟一个数字或META后跟一个数字解释为传递该数字给函数作为其参数的命令。Emacs预定义了超过二十个字符供与interactive配合使用。几乎在所有情况下,这些选项中的一个都能够使你交互式地向函数传递正确的信息。(参见《GNU Emacs Lisp参考手册》中的“interactive代码字符”部分。)
考虑zap-to-char函数,它的interactive表达式是:
(interactive "p\ncZap to char: ")
interactive参数的第一部分是熟悉的"p"。这个参数告诉Emacs将前缀解释为要传递给函数的数字。你可以通过键入C-u后跟一个数字或META后跟一个数字来指定前缀。前缀表示指定字符的数量。因此,如果你的前缀是3,并且指定的字符是'x',那么你会删除包括第三个'x'在内的所有文本。如果不设置前缀,则会删除直到并包括指定字符的所有文本,但不会更多。
这里的'c'告诉函数要删除到哪个字符为止。
更正式地说,具有两个或多个参数的函数可以通过在interactive后面的字符串中添加部分来将信息传递给每个参数。当你这样做的时候,信息会按照interactive列表中指定的顺序传递给每个参数。在字符串中,每个部分都由'\n'(换行符)与下一个部分分隔开。例如,你可以在'p'后面跟随'\n'和'cZap to char: '。这会导致Emacs将前缀参数的值(如果有)以及指定的字符传递给函数。
在这种情况下,函数定义看起来像下面这样,其中arg和char是interactive绑定前缀参数和指定字符的符号:
(defun name-of-function (arg char)
"文档说明……"
(interactive "p\ncZap to char: ")
函数主体…)
(提示中的冒号后的空格可以使你在被提示时显示得更好。请参阅copy-to-buffer函数的定义示例。)
当函数不接受任何参数时,interactive不需要任何内容。这样的函数包含简单的表达式(interactive)。mark-whole-buffer函数就是这样。
另外,如果预定义的特殊字母编码不适合你的应用,你可以将自定义参数以列表形式传递给interactive。
请参阅append-to-buffer函数的定义示例。欲了解更多关于此技术的详细解释,请参阅《GNU Emacs Lisp参考手册》中的“Using Interactive”章节。
3.5 永久安装代码
当你通过评估来安装函数定义时,该函数定义会一直保持安装状态,直到你退出Emacs。下次启动新的Emacs会话时,除非再次评估函数定义,否则该函数将不会被自动安装。
在某个时刻,你可能希望每次启动新Emacs会话时都自动安装某些代码。有几种方法可以实现这一目标:
- 如果你的代码仅为自己使用,你可以将函数定义的代码放入你的.emacs初始化文件中。当你启动Emacs时,.emacs文件会被自动评估,其中的所有函数定义都会被安装。详情参见“你的.emacs文件”部分。
- 另一种方式是,你可以将想要安装的函数定义放在一个或多个独立的文件中,并使用load函数使Emacs评估这些文件中的每个函数,从而进行安装。详情参见“加载文件”部分。
- 第三种情况,如果你拥有整个站点都将使用的代码,则通常将其放置在一个名为site-init.el的文件中,该文件在构建Emacs时会被加载。这样,所有使用你的机器的人都能使用这段代码。(请参考Emacs发行版的一部分——INSTALL文件。)
最后,如果你拥有的代码是所有Emacs用户可能都需要的,你可以将其发布到计算机网络上,或者发送一份副本给自由软件基金会。(当你这样做时,请确保代码及其文档遵循允许他人运行、复制、学习、修改和重新分发的许可协议,并保护你自己免受作品被无偿拿走的风险。)如果你向自由软件基金会发送了一份你的代码副本,并且恰当地保护了自己和其他人,那么它可能会被包含在下一个Emacs版本中。在过去几年里,Emacs正是通过这样的捐赠,在很大程度上不断发展壮大。
3.6 let表达式
在Lisp中,let表达式是一种特殊形式,大多数函数定义中你都会需要用到它。
let用于将符号绑定到某个值上,这样Lisp解释器就不会混淆该变量与不属于该函数的同名变量。
为了理解为什么需要let这种特殊形式,可以考虑以下情况:假设你拥有一所房子,通常你会称之为“the house”,例如,“The house needs painting(这栋房子需要粉刷)”。当你去朋友家做客时,如果你的朋友也说到“the house”,他很可能指的是他的房子,而不是你的房子,也就是说,指代的是不同的房子。
如果朋友提到的是他自己的房子,而你误以为是指你的房子,可能会产生混淆。同样的问题在Lisp中也可能发生,如果一个函数内部使用的变量与另一个函数内部使用的变量名称相同,但两者并不打算指向相同的值。let特殊形式就是为了防止这类混淆的发生。
let防止混淆
let特殊形式可以防止混淆。它创建了一个局部变量的名称,该名称在let表达式内部会遮盖(或覆盖)外部同名变量的使用(在计算机科学术语中,我们称此为绑定变量)。这就像理解当你在朋友家时,每当他提到“the house”时,他指的是他自己的房子,而不是你的房子一样。(用于命名函数参数的符号同样以相同方式作为局部变量进行绑定。参见The defun Macro部分。)
另一种理解let的方式是,它定义了代码中的一个特殊区域:在let表达式体内,你所命名的变量具有自身的局部含义。在let体外部,它们有其他的含义(或者可能根本未被定义)。这意味着在let体内部,对由let表达式命名的变量调用setq将会设置该名称局部变量的值。然而,在let体外部(如调用在其他地方定义的函数时),对由let表达式命名的变量调用setq将不会影响那个局部变量。
let可以同时创建多个变量,并且每个变量在创建时都会被赋予一个初始值,这个值既可以是你指定的,也可以是nil(在行话中,这被称为将变量绑定到值上)。在let创建并绑定完变量后,它会执行let体内的代码,并返回let体中最后一个表达式的值,作为整个let表达式的值。(“执行”是一个术语,意指计算列表;这个词来源于“给予实际效果”的意思(来自牛津英语词典)。由于评估表达式是为了执行操作,“执行”逐渐演化成了“评估”的同义词。)
3.6.1 let表达式的组成部分
let表达式由三个部分组成。第一部分是符号let。第二部分是一个列表,称为varlist,其中每个元素要么是一个单独的符号,要么是一个包含两个元素的列表,其第一个元素为符号。let表达式的第三部分是let体。let体通常由一个或多个列表构成。
let表达式的一个模板如下所示:
(let varlist body…)
varlist中的符号是由let特殊形式赋予初始值的变量。单独出现的符号会被赋予初始值nil;而作为两元素列表中第一个元素的符号会绑定到Lisp解释器评估第二个元素时返回的值。
因此,一个varlist可能看起来像这样:(thread (needles 3))。在这种情况下,在let表达式中,Emacs将符号thread绑定到初始值nil,并将符号needles绑定到初始值3。
当你编写let表达式时,你需要在let表达式模板的相应位置填入适当的表达式。
如果varlist由两元素列表组成(这种情况很常见),那么let表达式的模板则看起来像这样:
(let ((变量 值)
(变量 值)
…)
body…)
3.6.2 示例let表达式
下面的表达式创建并赋予两个变量zebra和tiger初始值。let表达式的体是一个调用message函数的列表。
(let ((zebra "stripes")
(tiger "fierce"))
(message "One kind of animal has %s and another is %s."
zebra tiger))
在这里,varlist为((zebra "stripes") (tiger "fierce"))。
这两个变量分别是zebra和tiger。每个变量都是一个两元素列表的第一个元素,而每个值则是其对应两元素列表的第二个元素。在varlist中,Emacs将变量zebra绑定到值"stripes"上,并将变量tiger绑定到值"fierce"上。在这个例子中,这两个值都是字符串。这些值同样可以是其他列表或符号。let表达式的体紧跟在存储变量的列表之后。在这个例子中,体是一个使用message函数在回显区打印字符串的列表。
你可以按照常规方式评估这个示例,即把光标置于最后一个括号后,然后键入C-x C-e。当你这样做时,回显区将会显示以下内容:
"One kind of animal has stripes and another is fierce."
如我们之前所见,message函数会打印它的第一个参数,但不包括‘%s’。在这个例子中,变量zebra的值被打印在第一个‘%s’的位置,变量tiger的值被打印在第二个‘%s’的位置。
脚注
(10)
根据Jared Diamond在《枪炮、病菌与钢铁》中的说法,“…随着年龄的增长,斑马变得几乎无法驾驭”,但此处的说法是它们并不会像老虎那样凶猛。(1997年,W. W. Norton and Co.出版社出版,ISBN 0-393-03894-2,第171页)
3.6.3 let语句中未初始化的变量
如果你在let语句中没有将变量绑定到特定的初始值,它们会自动被绑定到初始值nil,如下例所示:
(let ((birch 3)
pine
fir
(oak 'some))
(message
"Here are %d variables with %s, %s, and %s value."
birch pine fir oak))
此处,varlist为((birch 3) pine fir (oak 'some))。
若按照常规方式评估这个表达式,你的回显区将会显示以下内容:
"Here are 3 variables with nil, nil, and some value."
在这个示例中,Emacs将符号birch绑定到数字3,将符号pine和fir绑定到nil,并将符号oak绑定到值some。
请注意,在let的第一部分中,变量pine和fir作为原子单独出现且不被括号包围;这是因为它们被绑定到了空列表nil。但oak被绑定到了some,因此它属于列表(oak 'some)的一部分。同样地,birch被绑定到了数字3,因此它与该数字在一个列表内。(由于数字本身会被评估为其自身,所以无需引用数字。另外,消息中使用‘%d’而不是‘%s’来打印数字。)这四个变量作为一个整体被放入一个列表中,以区分它们与let体的边界。
3.6.4 let如何绑定变量
Emacs Lisp支持两种不同的方式将变量名与其值关联起来。这两种方式会影响特定绑定在程序中有效的作用域。由于历史原因,默认情况下,Emacs Lisp使用了一种称为动态绑定的变量绑定形式。然而,在本手册中,除非另有说明,我们讨论的是首选的绑定形式,即词法绑定(在未来,Emacs维护者计划将默认绑定更改为词法绑定)。如果你之前接触过其他编程语言,可能已经对词法绑定的行为有所了解。
要在程序中使用词法绑定,应在Emacs Lisp文件的第一行添加以下内容:
;;; -*- lexical-binding: t -*-
有关这方面的更多信息,请参阅《Emacs Lisp参考手册》中的“变量作用域”部分。
词法绑定与动态绑定的区别
如前所述(见“let防止混淆”),在词法绑定下使用let创建局部变量时,这些变量仅在let表达式体内有效。在代码的其他部分,它们具有不同的含义,因此如果你在let体内部调用了一个在其他地方定义的函数,那么该函数将无法“看到”你创建的局部变量。(另一方面,如果你调用了在let体内部定义的一个函数,那个函数将能够查看并修改来自该let表达式的局部变量。)
而在动态绑定下,规则有所不同:当你使用let时,创建的局部变量在其执行期间都是有效的。这意味着,如果let表达式调用了某个函数,无论该函数在哪里定义(包括在完全不同的文件中),它都能够访问到这些局部变量。
从另一个角度看,在使用动态绑定时的let表达式,可以想象每个变量名都有一个全局的“堆栈”式的绑定列表,每当你使用这个变量名时,它引用的是堆栈顶部的绑定。(你可以把这个想象成桌面上一叠写有值的纸张。)当你使用let动态地绑定变量时,它会将你指定的新绑定置于堆栈的顶部,然后执行let体内的代码。一旦let体执行完毕,它会从堆栈中移除这一绑定,揭示出(如果有)let表达式之前该变量所具有的绑定。
示例:词法绑定与动态绑定的区别
在某些情况下,词法绑定和动态绑定的行为完全相同。然而,在其他情况下,它们可能会改变程序的含义。例如,下面这段代码展示了在词法绑定下会发生的情况:
;;; -*- lexical-binding: t -*-
(setq x 0)
(defun getx ()
x)
(setq x 1)
(let ((x 2))
(getx))
⇒ 1
在此例中,(getx) 的结果是 1。在词法绑定下,getx 函数无法看到 let 表达式中的 x 的值。这是因为 getx 函数体位于我们 let 表达式外部。由于 getx 在代码的顶层(即不在任何 let 表达式内部)定义,因此它也在全局级别查找并找到 x。执行 getx 时,当前全局 x 的值为 1,所以这就是 getx 返回的结果。
如果我们改为使用动态绑定,则行为会有所不同:
;;; -*- lexical-binding: nil -*-
(setq x 0)
(defun getx ()
x)
(setq x 1)
(let ((x 2))
(getx))
⇒ 2
现在,(getx) 的结果变为 2!这是因为,在动态绑定下,当执行 getx 时,栈顶当前对 x 的绑定来自我们的 let 绑定。这次,getx 不会看到全局 x 的值,因为在绑定栈中,它的绑定位于 let 表达式的绑定之下。
(有些变量也被称为“特殊”变量,即使 lexical-binding 设置为 t,它们也会始终动态绑定。请参阅使用 defvar 初始化变量。)
3.7 特殊形式 if
另一个特殊形式是条件判断 if。这种形式用于指导计算机做出决策。在定义函数时可以不使用 if,但由于其使用的频率较高且重要性较大,因此在这里予以介绍。例如,在函数 beginning-of-buffer 的代码中就使用了 if。
if 形式背后的基本思想是,如果一个测试条件为真,则会求值一个表达式。如果测试条件不为真,则该表达式不会被求值。例如,你可以做出这样的决策:“如果天气温暖晴朗,那么就去海滩!”(在编程语境下,转换为程序逻辑则是:如果满足某个条件,就执行某段代码;如果不满足条件,则不执行这段代码。)
(下面这段是通义千问自己添加的)
具体到 Emacs Lisp 中的 if 特殊形式,其语法结构如下:
(if test-expr then-expr [else-expr])
这里的含义是:如果 test-expr 求值结果为非 nil,则求值并返回 then-expr 的结果;如果 test-expr 求值结果为 nil,则可选地求值并返回 else-expr 的结果。如果省略了 else-expr,当 test-expr 为 nil 时,if 表达式将返回 nil。
if 的详细说明
在 Lisp 中编写的 if 表达式并不会使用“then”这个词;测试部分和执行动作是列表中第一个元素为 if 的后续两个元素。尽管如此,在 if 表达式中,测试部分通常被称为 if 部分,第二个参数则常被称作 then 部分。
此外,编写 if 表达式时,真或假的测试条件通常与符号 if 写在同一行上,但如果测试为真的执行动作(即 then 部分)则写在第二行及后续行上。这样可以使 if 表达式更易于阅读。
(if true-or-false-test
action-to-carry-out-if-test-is-true)
true-or-false-test 是一个会被 Lisp 解析器求值的表达式。
以下是一个可以按常规方式求值的例子。测试内容是判断数字 5 是否大于数字 4。由于确实如此,将打印出消息 '5 is greater than 4!'。
(if (> 5 4) ; if-part
(message "5 is greater than 4!")) ; then-part
(函数 > 用于测试其第一个参数是否大于第二个参数,并在满足条件时返回真值。)
当然,在实际应用中,if 表达式中的测试条件不会像表达式 (> 5 4) 这样固定不变。相反,至少会有一个在测试中使用的变量被绑定到一个事先未知的值上。(如果这个值能提前知道,那么我们就不需要运行测试了!)
例如,这个值可能被绑定到函数定义的一个参数上。在以下函数定义中,动物的性格特征是一个传递给函数的值。如果 characteristic 绑定到的值为 "fierce",那么将打印出消息 'It is a tiger!';否则,将返回 nil。
(defun type-of-animal (characteristic)
"根据 CHARACTERISTIC 在回显区打印相应消息。
如果 CHARACTERISTIC 是字符串 \"fierce\",
则警告有老虎出现。"
(if (equal characteristic "fierce")
(message "It is a tiger!")))
如果你正在 GNU Emacs 中阅读这段内容,你可以按照常规方式求值这个函数定义以将其安装到 Emacs 中,然后可以求值以下两个表达式查看结果:
(type-of-animal "fierce")
(type-of-animal "striped")
当你求值 (type-of-animal "fierce") 时,会在回显区看到打印的消息:"It is a tiger!";而当你求值 (type-of-animal "striped") 时,则会在回显区看到打印的 nil。
3.7.1 type-of-animal 函数详解
让我们详细分析一下 type-of-animal 函数。
type-of-animal 函数的定义是通过填充两个模板的槽位完成的,一个是用于整体函数定义的模板,另一个则是用于 if 表达式的模板。
非交互式函数的一般模板如下:
(defun 函数名 (参数列表)
"文档字符串……"
函数体…)
与该模板对应的函数部分如下所示:
(defun type-of-animal (特征)
"根据 CHARACTERISTIC 在回显区打印消息。
如果 CHARACTERISTIC 是字符串 \"fierce\",
则警告有老虎出现。"
函数体:if 表达式)
函数名称为 type-of-animal,它接收一个参数的值。参数列表后面跟着一个多行的文档字符串。由于为每个函数定义编写文档字符串是一个良好的习惯,所以在示例中包含了文档字符串。函数定义的主体由 if 表达式组成。
if 表达式的一般模板如下:
(if 真或假测试条件
测试结果为真时执行的动作)
在 type-of-animal 函数中,if 表达式的代码如下:
(if (equal 特征 "fierce")
(message "It is a tiger!"))
这里的真或假测试条件是表达式:
(equal 特征 "fierce")
在 Lisp 中,equal 是一个函数,用于判断其第一个参数是否等于第二个参数。第二个参数是字符串 "fierce",而第一个参数是符号 characteristic 的值——换句话说,就是传递给这个函数的参数。
在 type-of-animal 函数的第一个练习中,将参数 "fierce" 传递给 type-of-animal 函数。因为 "fierce" 等于 "fierce",所以表达式 (equal 特征 "fierce") 返回真值。当这种情况发生时,if 表达式会求值第二个参数或 then 部分:(message "It is a tiger!"),并在回显区打印出相应的消息。
另一方面,在 type-of-animal 函数的第二个练习中,将参数 "striped" 传递给 type-of-animal 函数。由于 "striped" 不等于 "fierce",所以 then 部分不会被求值,并且 if 表达式返回 nil。
3.8 条件表达式(If-then-else)
条件表达式if可以有一个可选的第三个参数,称为else-part,用于处理真值测试结果为假的情况。当这种情况发生时,整个if表达式的第二个参数或then-part不会被执行,而第三个或else-part会被执行。你可以将其理解为决策中的“如果天气温暖晴朗,就去海滩;否则,就读一本书!”的阴天备选方案。
在Lisp代码中并不会写出“else”这个词;if表达式的else-part位于then-part之后。在书面形式的Lisp代码中,通常else-part会另起一行,并且缩进程度小于then-part:
(if 真值测试表达式
测试为真时执行的动作
测试为假时执行的动作)
例如,以下if表达式在你以常规方式求值时,会打印出消息‘4不大于5!’:
(if (> 4 5) ; if-part
(message "4错误地大于5!") ; then-part
(message "4 is not greater than 5!")) ; else-part
注意,不同级别的缩进使得区分then-part和else-part变得容易。(GNU Emacs 提供了多个自动正确缩进if表达式的命令。请参阅GNU Emacs Helps You Type Lists。)
我们可以通过简单地在if表达式中添加额外部分来扩展type-of-animal函数,使其包含一个else-part。
如果你评估以下type-of-animal函数定义的版本来安装它,然后评估随后的两个表达式以向函数传递不同的参数,你可以看到这样做的结果。
(defun type-of-animal (characteristic) ; 第二个版本
"根据CHARACTERISTIC在回显区打印消息。
如果CHARACTERISTIC是字符串\"fierce\",
则警告有老虎出现;否则表示不够凶猛。"
(if (equal characteristic "fierce")
(message "It is a tiger!")
(message "It is not fierce!")))
(type-of-animal "fierce")
(type-of-animal "striped")
当你求值(type-of-animal "fierce")时,你会在回显区看到如下消息:“It is a tiger!”;但当你求值(type-of-animal "striped")时,你会看到“It is not fierce!”的消息。
(当然,如果特性是"ferocious",则会打印出消息"It is not fierce!";在这种情况下信息可能会产生误导!在编写代码时,你需要考虑到可能有些类似的参数会被if测试,并据此相应地编写程序。)
3.9 Emacs Lisp中的真值与假值
在if表达式的真值测试中有一个重要的方面。至今为止,我们一直将“真”和“假”作为谓词的值来讨论,仿佛它们是Emacs Lisp中新类型的对象。实际上,“假”就是我们熟悉的老朋友nil。除此之外的一切——任何东西——都被视为“真”。
如果真值测试表达式求值结果不是nil,那么这个表达式就被解释为真。换句话说,只要返回的值是一个数字(如47)、字符串(如"hello")、符号(除了nil之外,如flowers)、列表(只要它不为空)甚至是缓冲区,测试的结果都将被认为是真。
总结来说,在Emacs Lisp中,只有当一个表达式求值结果为nil时,该表达式在if语句中被视为假;否则,无论其求值结果为何种类型的对象,都会被当作真处理。
nil的解释
在展示真值测试之前,我们需要先解释一下nil。
在Emacs Lisp中,符号nil具有两种含义。首先,它表示空列表。其次,它表示假,并且当一个真值测试结果为假时返回该值。nil可以写作一个空列表(()),也可以写作nil。就Lisp解释器而言,()和nil是等价的。然而,在实际使用中,人类倾向于用nil表示假,用()表示空列表。
在Emacs Lisp中,任何非nil——即非空列表——的值都被视为真。这意味着如果一个表达式的求值结果不是一个空列表,那么if表达式将检测为真。例如,如果在一个测试位置放置了一个数字,这个数字会被求值并返回自身,因为这是数字被求值时的行为。在这种条件下,if表达式将检测为真。只有当表达式求值结果为nil或空列表时,if表达式才会检测为假。
通过评估以下示例中的两个表达式,你可以看到这一点。
在第一个示例中,数字4作为if表达式中的测试项被求值并返回自身;因此,表达式的then-part部分被执行并返回:“true”出现在回显区。在第二个示例中,nil表示假;因此,表达式的else-part部分被执行并返回:“false”出现在回显区。
(if 4
'true
'false)
(if nil
'true
'false)
顺便说一下,如果某个用于表示真值测试结果为真的有用值不可用,则Lisp解释器会返回符号t来表示真。例如,表达式(> 5 4)在被正常方式求值时返回t,你可以通过以下方式验证这一点:
(> 5 4)
另一方面,如果测试结果为假,这个函数则会返回nil。
(> 4 5)
3.10 save-excursion
save-excursion函数是本章将要讨论的最后一个特殊形式。
在用于编辑的Emacs Lisp程序中,save-excursion函数是非常常见的。它保存光标的位置,执行函数体,然后如果光标的当前位置发生变化,则将其恢复到先前的位置。它的主要目的是防止由于光标的意外移动而让用户感到惊讶和困扰。
光标与标记
然而,在讨论save-excursion之前,首先回顾一下GNU Emacs中光标(point)和标记(mark)的概念可能会有所帮助。光标是指当前的光标位置。无论光标在哪里,那里就是point的位置。更准确地说,在那些光标看起来位于字符上方的终端上,point位于该字符之前。在Emacs Lisp中,point是一个整数。缓冲区中的第一个字符编号为1,第二个字符编号为2,以此类推。函数point返回光标的当前位置作为数字。每个缓冲区都有自己的point值。
标记是缓冲区中的另一个位置;可以通过诸如C-SPC(set-mark-command)之类的命令设置其值。如果已设置了标记,可以使用命令C-x C-x(exchange-point-and-mark)使光标跳转到标记处,并将标记设置为先前的point位置。此外,如果你设置了另一个标记,先前标记的位置会被保存在标记环中。这样可以保存许多标记位置。通过连续多次键入C-u C-SPC,你可以使光标跳转到已保存的标记处。
point与mark之间的缓冲区部分被称为区域(region)。包括center-region、count-words-region、kill-region和print-region在内的众多命令都作用于这个区域。
save-excursion特殊形式会保存point的位置,并在Lisp解释器评估该特殊形式内部代码后恢复这个位置。因此,如果point位于一段文本的开头,而某些代码将point移动到了缓冲区末尾,save-excursion会在函数体中表达式求值完毕后将point恢复到原来的位置。
在Emacs中,即使用户并未预期,函数也常常在其内部工作过程中移动point。例如,count-words-region就会移动point。为了防止用户因意外且从用户角度来看不必要的跳跃感到困扰,save-excursion经常被用来保持point位于用户期望的位置。使用save-excursion是一种良好的编程习惯。
为了确保程序运行顺畅,save-excursion甚至会在内部代码出现错误时(或者更精确地讲,用专业术语来说,“在异常退出的情况下”)恢复point的值。这一特性非常有用。
除了记录point的值之外,save-excursion还会跟踪并恢复当前缓冲区。这意味着你可以编写改变缓冲区的代码,并利用save-excursion将其切换回原始缓冲区。这就是append-to-buffer中使用save-excursion的方式。(参见append-to-buffer的定义部分。)
3.10.1 save-excursion表达式的模板
使用save-excursion的代码模板很简单:
(save-excursion
body…)
函数体包含一个或多个表达式,这些表达式将由Lisp解释器按顺序进行求值。如果函数体中包含多个表达式,则最后一个表达式的值将作为save-excursion函数的返回值。函数体中的其他表达式仅为了其副作用而被求值;而save-excursion本身仅用于其副作用(即恢复光标位置)。
更具体地说,save-excursion表达式的模板如下所示:
(save-excursion
第一个函数体中的表达式
第二个函数体中的表达式
第三个函数体中的表达式
…
最后一个函数体中的表达式)
当然,一个表达式可以是独立的符号,也可以是一个列表。
在Emacs Lisp代码中,save-excursion表达式常常出现在let表达式的体内。它看起来像这样:
(let 变量列表
(save-excursion
函数体内容…))
3.11 复习
在过去的几章中,我们引入了一个宏以及相当数量的函数和特殊形式。接下来将简要回顾这些内容,并补充一些尚未提及的相似函数。
-
eval-last-sexp
该命令计算当前光标位置前的最后一个符号表达式并返回其值。如果没有为该函数提供参数,则结果会在回显区打印;如果提供了参数,则输出会显示在当前缓冲区中。此命令通常绑定到C-x C-e快捷键上。
-
defun
定义函数。这个宏包含最多五个部分:函数名、传给函数的参数模板、文档字符串、可选的交互式声明及函数定义体。
例如,在Emacs中,dired-unmark-all-marks函数的定义如下所示:
(defun dired-unmark-all-marks ()
"从Dired缓冲区中的所有文件中移除所有标记。"
(interactive)
(dired-unmark-all-files ?\r))
-
interactive
声明函数可以被交互式调用。这个特殊形式后面可以跟一个字符串,其中包含一个或多个组成部分,按顺序传递信息给函数的参数,并可能指示解释器请求用户输入信息。字符串各部分由换行符('\n')分隔开。
常见的代码字符包括:
- b:表示现有缓冲区的名字。
- f:表示现有文件的名字。
- p:表示数字前缀参数(注意这里的p是小写字母)。
- r:表示光标位置(point)和标记,作为两个连续的数字参数,先给出较小的那个值。
关于交互式代码字符的完整列表,请参阅GNU Emacs Lisp参考手册中关于“interactive”的Code Characters说明。
-
let
在let表达式的主体内声明一组变量并赋予它们初始值,可以是nil或者指定的值;然后计算let主体内的其余表达式,并返回最后一个表达式的值。在let表达式的主体内部,Lisp解释器不会看到与外部同名变量的绑定值。
例如,
(let ((foo (buffer-name))
(bar (buffer-size)))
(message
"当前缓冲区名为%s,共有%d个字符。"
foo bar))
-
save-excursion
在评估此特殊形式主体之前记录光标位置(point)和当前缓冲区的值,在主体评估完成后恢复这两个值。
例如,
(message "我们在缓冲区的第%d个字符处。"
(- (point)
(save-excursion
(goto-char (point-min)) (point))))
-
if
计算函数的第一个参数,若其为真,则继续计算第二个参数;否则如果有第三个参数,则计算它。
if特殊形式被称为条件语句。Emacs Lisp中存在其他条件语句,但if可能是最常用的。
例如,
(if (= 22 emacs-major-version)
(message "这是版本22的Emacs")
(message "这不是版本22的Emacs"))
-
<, >, <=, >=
函数<用于测试其第一个参数是否小于第二个参数;对应的函数>用于测试第一个参数是否大于第二个参数;<=测试第一个参数是否小于等于第二个;而>=测试第一个参数是否大于等于第二个。在所有这些情况下,两个参数必须都是数字或标记(标记表示缓冲区中的位置)。
-
=
函数=用于测试两个参数(均为数字或标记)是否相等。
-
equal, eq
这两个函数用于测试两个对象是否相同。equal使用了“相同”一词的一种含义,即当两个对象具有类似的结构和内容时返回真,比如两本相同书籍的副本。而eq仅在两个参数实际上是同一个对象时返回真。
-
string<, string-lessp, string=, string-equal
string-lessp函数用于测试其第一个参数是否小于第二个参数。string-lessp有一个更短的别名(通过defalias实现),即string<。
string-lessp的参数必须是字符串或符号;排序方式是字典序,因此大小写是有意义的。比较符号时使用的是符号的打印名字而非符号本身。
空字符串("")比任何非空字符串都要小。
string-equal提供了对应于相等性测试的功能,其较短的别名是string=。目前没有提供直接对应于>、>=或<=操作的字符串测试函数。
-
message
在回显区域打印一条消息。第一个参数是一个字符串,其中可以包含‘%s’、‘%d’或‘%c’来打印紧跟在字符串后面的参数的值。‘%s’所引用的参数必须是字符串或符号;‘%d’所引用的参数必须是数字;‘%c’所引用的参数必须是ASCII码数值。(还有一些未提及的其他%-序列。)
-
setq, set
setq特殊形式设置其第一个参数的值为第二个参数的值。setq会自动引用第一个参数。对于后续每对参数也会执行同样的操作。
-
buffer-name
若不提供参数,则以字符串形式返回当前缓冲区的名字。
-
buffer-file-name
若不提供参数,则返回当前缓冲区正在访问的文件名。
-
current-buffer
返回Emacs当前激活的缓冲区;这可能不是屏幕上可见的缓冲区。
-
other-buffer
返回最近选择的缓冲区(除了作为other-buffer参数传入的缓冲区以及当前缓冲区之外)。
-
switch-to-buffer
选择一个缓冲区让Emacs激活并在当前窗口中显示,以便用户查看。通常绑定到C-x b快捷键上。
-
set-buffer
将Emacs的焦点切换到程序将运行的缓冲区上,不改变当前窗口的显示内容。
-
buffer-size
返回当前缓冲区中的字符数。
-
point
返回当前光标位置的值,作为一个整数,表示从缓冲区开始处算起的字符数。
-
point-min
返回当前缓冲区中点所能取到的最小值。除非进行了窄化操作,否则该值为1。
-
point-max
返回当前缓冲区中点所能取到的最大值。除非进行了窄化操作,否则该值为缓冲区末尾的位置。
3.12 练习
-
编写一个非交互式的函数,该函数接受一个数字作为参数,并将其值翻倍。然后将这个函数改造为可交互式调用的函数。
示例代码:
(defun double-number (number)
"Non-interactive function that doubles the argument."
(* number 2))
(defun interactive-double (arg)
"Interactive function to double a number."
(interactive "nNumber to double: ")
(double-number arg))
-
编写一个函数,检查当前
fill-column的值是否大于传递给函数的参数值,如果是,则打印一条适当的提示信息。示例代码:
(defun check-fill-column-greater-than (column-value)
"Tests if fill-column is greater than COLUMN-VALUE and prints a message accordingly."
(let ((current-fill-column fill-column))
(if (> current-fill-column column-value)
(message "The current fill-column value (%d) is greater than the passed value (%d)." current-fill-column column-value)
(message "The current fill-column value (%d) is not greater than the passed value (%d)." current-fill-column column-value))))
(备注:示例代码是通义千问生成的)
浙公网安备 33010602011771号