【Elisp官方指南,翻译】1 列表处理
原文地址:https://www.gnu.org/software/emacs/manual/html_node/eintr/List-Processing.html
备注:使用GPT-3.5-Turbo进行翻译,略微修正。
1 列表处理
对于没有接受过培训的人来说,Lisp 是一种奇怪的编程语言。在 Lisp 代码中,到处都是括号。甚至有些人声称这个名字代表“很多孤立的愚蠢的括号”。但这个说法是不准确的。Lisp 代表着LISt处理,这种编程语言通过把列表(以及列表的列表)放在括号中来处理它们。括号标志着列表的边界。有时,列表前面还会有一个名为单引号的撇号‘'’。列表是 Lisp 的基础。
- Lisp列表
- 运行程序
- 生成错误消息
- 符号名称和函数定义
- Lisp解释器
- 评估
- 变量
- 参数
- 设置变量的值
- 摘要
- 练习
脚注(1)
单引号是特殊形式quote的缩写;您现在不需要考虑特殊形式。参见复杂情况。
1.1 Lisp列表
在Lisp中,一个列表看起来像这样:'(rose violet daisy buttercup)。这个列表前面有一个单引号。它也可以被写成以下形式,更像你可能熟悉的列表类型:
'(rose
violet
daisy
buttercup)
这个列表中的元素是四种不同的花的名称,它们之间用空格分隔,并用括号括起来,就像田间的花围绕着一堵石墙一样。
数字,列表内部的列表
列表中也可以包含数字,就像这个列表中的列表:(+ 2 2)。 这个列表有一个加号“+”,后面跟着两个“2”,每个之间用空格分隔。
在Lisp中,数据和程序都以相同的方式表示; 也就是说,它们都是单词、数字或其他列表的列表,它们之间由空格分隔,并用括号括起来。(由于程序看起来像数据,因此一个程序很容易作为另一个程序的数据;这是Lisp的一个非常强大的特性。)(顺便说一句,这两个括号注释不是Lisp列表,因为它们包含 “;” 和 “.” 作为标点符号。)
这里是另一个列表,这次它里面有一个列表:
'(this list has (a list inside of it))
这个列表的组成部分是单词this’, ‘list’, ‘has’,和列表‘(a list inside of it)’。里面的列表由单词‘a’, ‘list’, ‘inside’, ‘of’, ‘it’组成。
1.1.1 Lisp原子
在Lisp中,我们一直称为单词的东西称为原子。这个术语来自于原子的历史意义,它意味着“不可分割”。就Lisp而言,我们在列表中使用的单词不能分解为任何更小的部分,而在程序的上下文中仍然具有相同的含义;这也适用于数字和单个字符符号,比如“+”。另一方面,不像古代的原子,列表可以分割成部分。(参见car cdr&cons基本函数。)
在列表中,原子之间由空格分隔。它们可以紧邻括号。
从技术上讲,Lisp中的列表由括号包围,括号中是由空格分隔的原子,或者是其他列表,或者是既包围原子又包围其他列表。列表中可以只有一个原子,或者根本没有东西。一个没有东西的列表看起来像这样:(),称为空列表。与任何其他东西不同,空列表同时被认为是原子和列表。
原子和列表的打印表示称为符号表达式,或者更简练地称为s-表达式。表达式本身可以指打印表示,也可以指计算机内部持有的原子或列表。人们常常不加区分地使用表达式这个说法。(另外,在许多文本中,字形一词被用作表达的同义词。)
顺便说一句,构成我们宇宙的原子在被认为是不可分割时被命名;但是后来发现,物理原子并不是不可分割的。部分原子可以分裂出来,或者裂变成两个大致相同大小的部分。物理原子是过早地被命名的,因为在发现其更真实的本质之前。在Lisp中,某些类型的原子,比如数组,可以被分解成部分;但分解这种机制与分割列表的机制是不同的。就列表运算而言,列表的原子是不可分割的。
和英语一样,Lisp原子的组成字母的意义不同于这些字母作为一个词的意义。例如,南美树懒的词,‘ai’,与两个词‘a’和‘i’完全不同。
自然界中有许多种原子,但Lisp中只有几种:例如数字,比如37,511或1729,以及符号,比如‘+’,‘foo’或‘forward-line’。我们在上面的例子中列出的单词都是符号。在日常的Lisp会话中,很少使用单词“原子”,因为程序员通常都试图更具体地说明他们正在处理的原子是什么类型。Lisp编程主要是关于列表内的符号(有时也是数字)。 (顺便说一句,上面的三个字的括号备注在Lisp中是一个正确的列表,因为它由原子组成,在这种情况下是符号,由空格分隔,并用括号括起来,没有任何非Lisp标点。)
双引号之间的文本,即使是句子或段落,也是原子。这是一个例子:
'(this list includes "text between quotation marks.")
在Lisp中,所有引用的文本,包括标点和空格,构成一个单独的原子。这种原子称为字符串(用于“字符的字符串”),是计算机用于打印供人类阅读的消息的工具。字符串是一种与数字或符号不同的原子,并且使用方式也不同。
1.1.2 列表中的空格
列表中的空格数量无关紧要。从Lisp语言的角度来看,
'(this list
looks like this)
与这个完全相同:
'(this list looks like this)
这两个例子显示了Lisp语言中相同的列表是什么,该列表由‘this’,‘list’,‘looks’,‘like’和‘this’这几个符号按照这个顺序组成。
额外的空格和换行符是为了让列表更易于人类阅读。当Lisp读取表达式时,它会消除所有额外的空格(但必须在原子之间至少有一个空格以便将它们区分开来)。
奇怪的是,我们看到的例子几乎涵盖了Lisp列表的所有外观!Lisp中的每个其他列表看起来都多多少少像这些例子中的其中一个,只不过列表可能会更长,更复杂。简而言之,列表在括号之间,字符串在引号之间,符号看起来像一个词,数字看起来像一个数。(在某些情况下,可能会使用方括号、点和其他一些特殊字符;不过,我们在没有它们的情况下将走得相当远。)
1.1.3 GNU Emacs帮助您输入列表
当您在GNU Emacs中使用Lisp交互模式或Emacs Lisp模式输入Lisp表达式时,您可以使用一些命令来格式化Lisp表达式,使其易于阅读。 例如,按下TAB键会自动将光标所在的行进行正确的缩进。 通常将一个命令绑定到M-C-\,以便正确缩进区域中的代码。缩进设计得很好,以便您可以看到列表的哪些元素属于哪个列表 - 子列表的元素缩进比封闭列表的元素更多。
此外,当您输入一个右括号时,Emacs会暂时将光标跳回到匹配的左括号,这样您就可以看到是哪一个。 这非常有用,因为您在Lisp中输入的每个列表都必须使其右括号与左括号匹配。(有关更多信息,请参见GNU Emacs手册中的主要模式。)
1.2 运行程序
Lisp中的任何列表都是一个可以运行的程序。如果你运行它(在Lisp术语中称为评估),计算机将做三件事中的一件: 什么都不做, 只返回列表本身; 发送错误消息; 或者,将列表中的第一个符号视为执行某个命令。(当然,通常是你真正想要的最后一件事!)
我在前一节的一些示例列表前面加上的单引号', 被称为引用; 当它出现在列表之前时,它告诉Lisp什么都不要做,只需按照其编写的样子接受它。但如果在列表之前没有引号,列表的第一项就是特殊的: 它是计算机需要遵循的命令。(在Lisp中,这些命令称为函数。)前面提到的列表(+ 2 2)没有引号在其前面,所以Lisp理解+是一个指令,让计算机对列表后面的内容进行操作: 加上后面的数字。
如果你在GNU Emacs中的Info里看到这段文字,下面是如何对这样的列表进行评估: 将光标放在下面列表的右括号后面,然后输入C-x C-e:
(+ 2 2)
你会看到数字4出现在echo area2中。(你刚刚做的是对这个列表进行评估。回显区是屏幕底部显示或回显文本的那一行。)现在尝试对一个带引号的列表执行同样的操作: 将光标放在下面列表的右括号后面,然后键入C-x C-e:
'(this is a quoted list)
你会在echo area中看到(this is a quoted list)出现。
在这两种情况下,你所做的是给GNU Emacs中称为Lisp解释器的程序发送命令-让解释器对表达式进行评估。Lisp解释器的名称来源于一个人用来提出表达式意义的任务-一个解释者。
你还可以评估一个不作为列表一部分的原子,即没有用括号括起来的原子;同样,Lisp解释器将人类可读表达式转化为计算机语言。但在讨论这个问题之前(参见Variables),我们将讨论当你出现错误时,Lisp解释器会做什么。
脚注 (2)
Emacs以十进制数、八进制数、十六进制数,以及字符的形式显示整数值,但让我们暂时忽略这一便利功能。
1.3 生成错误消息
为了让你不必担心如果你不小心做了,现在我们会给Lisp解释器发送一个生成错误消息的命令。这是一个无害的活动;事实上,我们经常会故意尝试生成错误消息。一旦你理解了行话,错误消息就可以变得富有信息性。它们不应该被称为“错误”消息,而应该被称为“帮助”消息。它们就像是给在陌生国度上旅行的旅行者的指路牌;解读它们可能很困难,但一旦理解,它们可以指引方向。
错误消息由内置的GNU Emacs调试器生成。我们将进入调试器。你可以通过键入q来退出调试器。
我们要做的是评估一个没有引用且没有有意义的命令作为其第一个元素的列表。这是一个几乎与我们刚刚使用的相同的列表,但是没有单引号在它前面。将光标放在它后面,然后键入C-x C-e:
(this is an unquoted list)
Backtrace 窗口将弹出,你应该在其中看到以下内容:
---------- 缓冲区: *Backtrace* ----------
Debugger entered--Lisp error: (void-function this)
(this is an unquoted list)
eval((this is an unquoted list) nil)
elisp--eval-last-sexp(nil)
eval-last-sexp(nil)
funcall-interactively(eval-last-sexp nil)
call-interactively(eval-last-sexp nil nil)
command-execute(eval-last-sexp)
---------- 缓冲区: *Backtrace* ----------
你的光标将在此窗口中(你可能需要等待几秒钟才能看见它)。要退出调试器并关闭调试器窗口,请键入:
q
请现在键入q,这样你就可以确信自己可以退出调试器。然后,再次键入C-x C-e重新进入它。
基于我们已经了解的内容,我们几乎可以读懂这个错误消息了。
你要从Backtrace 缓冲区的下方向上阅读它;它告诉你Emacs做了什么。当你键入C-x C-e时,你发出了一个对eval-last-sexp命令的交互调用。eval是“evaluate(评估)”的缩写,sexp是“symbolic expression(符号表达式)”的缩写。这个命令的意思是“评估最后一个符号表达式”,也就是你的光标之前的表达式。
以上每一行告诉你Lisp解释器接下来评估了什么。最近的动作在最上面。这个缓冲区叫Backtrace缓冲区,因为它让你能够向后追踪Emacs。
在Backtrace缓冲区的顶部,你会看到这一行:
Debugger entered--Lisp error: (void-function this)
Lisp解释器试图评估列表的第一个原子,也就是单词‘this’。正是这个行为产生了错误消息‘void-function this’。
这条消息包含了‘void-function’和‘this’两个单词。
‘function’这个词之前曾经出现过一次。这是一个非常重要的词。就我们的目的而言,我们可以通过说一个函数是一组指示告诉计算机做某事来定义它。
现在我们可以开始理解这个错误消息了:‘void-function this’。这个函数(也就是单词‘this’)没有定义任何一组计算机要执行的指示。
这个略微奇怪的单词‘void-function’是设计成涵盖Emacs Lisp的实现方式,也就是当一个符号没有附加到它的函数定义,应该是void的,也就是应该是没有指示的。
另一方面,由于我们可以成功地对(+ 2 2)进行评估来得到2加2,我们可以推断出+号必须有一组指示让计算机遵从,并且那些指示必须是将+号之后的数字相加。
在这种情况下,你可以防止Emacs进入调试器。我们在这里不解释如何做到这一点,但我们将提到结果的外观,因为如果你在使用某些Emacs代码时遇到了一个bug,你可能会遇到类似的情况。在这种情况下,你将只会看到一行错误消息;它将出现在回显区,并且看起来像这样:
Symbol's function definition is void: this
只要你键入一个键,甚至只是移动光标,消息就会消失。
我们知道‘Symbol’这个词的意思。它指的是列表的第一个原子,也就是单词‘this’。‘function’这个词指的是指示计算机要做什么。 (从技术上讲,符号告诉计算机在哪里找到指示,但这是一个我们暂时可以忽略的复杂问题。)
这个错误消息可以理解:‘Symbol's function definition is void: this’。这个符号(也就是单词‘this’)缺乏计算机要执行的指示。
1.4 符号名称和函数定义
根据我们迄今为止讨论过的内容,我们可以表达关于Lisp的另一个特点 - 一个重要的特点:像+这样的符号本身并不是计算机执行的指令集。 相反,该符号被用作一种定位定义或指令集的方式,或许是暂时的。 我们看到的是通过这个名字可以找到这些指令。 人的名字工作原理相同。 我可以被称为“Bob”; 但是,我不是字母'B','o','b',而是,或曾是与特定生物形式一直相关联的意识。 那个名字不是我,但可以用来指代我。
在Lisp中,一组指令可以与多个名称相关联。 例如,添加数字的计算机指令可以链接到加号符号以及加号符号(在Lisp的某些方言中是这样)。 在人类中,我既可以被称为“Robert”,也可以被称为“Bob”,还可以用其他词汇来称呼。
另一方面,一个符号一次只能附加一个函数定义。 否则,计算机会对要使用的定义感到困惑。 如果在人类中是这样的话,世界上只能有一个人叫“Bob”。 但是,名称所指的函数定义可以很容易地更改。 (参见安装函数定义。)
由于Emacs Lisp非常庞大,通常习惯于以一种可以识别函数属于Emacs的部分的方式来命名符号。 因此,所有与Texinfo相关的函数的名称都以“texinfo-”开头,并且所有与读取邮件相关的函数的名称都以“rmail-”开头。
1.5 Lisp解释器
根据我们所见,我们现在可以开始弄清楚当我们命令Lisp解释器评估一个列表时它是如何做的。首先,它会查看列表前面是否有引号;如果有,解释器简单地给出了列表。另一方面,如果没有引号,解释器会查看列表中的第一个元素,并查看它是否有函数定义。如果有,解释器执行函数定义中的指令。否则,解释器会打印一个错误消息。
这就是Lisp的工作方式。简单。当然,还有一些额外的复杂性,我们一会儿会涉及到,但这些都是基本的。当然,要编写Lisp程序,你需要知道如何编写函数定义并将它们附加到名称,以及如何做到这一点而不会让自己或计算机感到困惑。
复杂情况
现在,让我们来谈谈第一个复杂情况。除了列表之外,Lisp解释器还可以计算未被引号引用且没有括号括住的符号。Lisp解释器会尝试确定该符号作为变量的值。这种情况在变量部分有描述。(参见变量)
第二个复杂情况是因为有些函数很不寻常,不能按照通常的方式工作。那些不行的称为特殊形式。它们用于特殊工作,例如定义一个函数,并且并不多。在接下来的几章中,你将会接触到一些比较重要的特殊形式。
除了特殊形式,还有宏。宏是在Lisp中定义的一种结构,与函数不同的地方在于宏将一个Lisp表达式转换为另一个表达式,该表达式将被替代原始表达式进行求值。 (见Lisp宏)
在本次介绍中,你不需要太担心某物是否为特殊形式、宏或普通函数。举例来说,if是一个特殊形式(见if特殊形式),但when是一个宏(见Lisp宏)。在Emacs的早期版本中,defun是一个特殊形式,但现在它是一个宏(见defun宏)。它仍然以相同的方式工作。
最后一个复杂情况是:如果Lisp解释器正在查看的函数不是一个特殊形式,并且如果它是列表的一部分,Lisp解释器会查看列表是否有一个嵌套的列表。如果有一个内部列表,Lisp解释器首先确定它应该如何处理内部列表,然后再处理外层列表。如果内部列表中还有另一个列表嵌套,它会首先处理那个列表,以此类推。它总是首先处理最内层的列表。解释器在内部列表上工作,以求得列表的结果。结果可能会被外层表达式使用。
否则,解释器会从左到右,从一个表达式到另一个表达式逐次处理。
1.5.1 字节编译
解释器的另一个方面是:Lisp解释器能够解释两种类型的实体:人类可读的代码,我们将专注于这种类型,以及特别处理过的代码,称为字节编译代码,这种代码是不可读的。字节编译代码运行速度比人类可读的代码更快。
您可以通过运行其中一个编译命令(如byte-compile-file)将人类可读的代码转换为字节编译代码。字节编译代码通常存储在以.elc扩展名结尾的文件中,而不是以.el结尾。您会在emacs/lisp目录中看到这两种类型的文件;要阅读的文件是带有.el扩展名的文件。
在实践中,对于大多数您可能想要为自定义或扩展Emacs做的事情,您不需要进行字节编译;我将不在此处讨论这个话题。有关字节编译的完整说明,请参阅《GNU Emacs Lisp参考手册》中的《Byte Compilation》。
1.6 评估
当Lisp解释器处理一个表达式时,这个活动的术语称为评估。我们说解释器“评估表达式”(evaluates the expression)。我之前几次使用过这个术语。这个词来自于日常语言中的使用,“确定价值或数量;评估”,根据韦伯斯特新大学词典。
Lisp 解释器的操作方式
在评估表达式之后,Lisp 解释器很可能会返回计算机根据在函数定义中找到的指令执行后产生的值,或者它可能放弃该函数并产生错误消息。(解释器也可能发现自己被“抛弃”,或者尝试无休止地重复自己的操作,形成无限循环。这些行为较少见,我们可以忽略不计。)最常见的情况是,解释器返回一个值。
与此同时,解释器返回一个值的同时,还可能执行其他操作,比如移动光标或者复制文件;这种其他类型的操作称为副作用。我们人类认为重要的操作,比如打印结果,通常对于 Lisp 解释器来说都是副作用。学习如何使用副作用相当容易。
总而言之,评估符号表达式最常见的结果是 Lisp 解释器返回一个值,并且可能执行一个副作用;否则就会产生错误。
1.6.1 评估内部列表
如果评估适用于另一个列表中的列表,则在评估外部列表时,外部列表可以使用第一个评估返回的值作为信息。这就解释了为什么内部表达式首先被评估:它们返回的值被外部表达式使用。
我们可以通过评估另一个加法示例来研究这个过程。在下面的表达式之后放置光标,然后输入C-x C-e:
(+ 2 (+ 3 3))
数字8将出现在回显区域。
发生的是Lisp解释器首先评估内部表达式 (+ 3 3),它返回值6;然后它评估外部表达式,就好像它写成了 (+ 2 6),这将返回值8。由于没有更多的封闭表达式要评估,解释器会将该值打印在回显区域中。
现在很容易理解通过按键C-x C-e调用的命令的名称:名称是eval-last-sexp。 sexp这几个字母是“符号表达式”的缩写,而eval是“评估”的缩写。该命令评估最后一个符号表达式。
作为一个实验,您可以尝试通过把光标放在紧接在表达式后面的下一行的开头,或者在表达式内部来评估表达式。
下面是表达式的另一个副本:
(+ 2 (+ 3 3))
如果将光标放在立即跟在表达式后面的空行的开头,然后输入C-x C-e,您仍将在回显区域中得到值8。现在尝试将光标放在表达式的内部。如果您把它放在倒数第二个括号的后面(这样它看起来就好像坐落在最后一个括号上),您将在回显区域中得到一个6!这是因为该命令评估了表达式 (+ 3 3)。
现在将光标放在一个数字的后面。输入C-x C-e,您将得到这个数字本身。在Lisp中,如果您评估一个数字,您将得到这个数字本身——这就是数字与符号不同的地方。如果您评估以一个符号开始的列表,您将得到一个返回值,这个返回值是计算机执行与该名称相关联的函数定义中的指令的结果。如果仅评估一个符号,会发生一些不同的事情,我们将在下一节中看到。
1.7 变量
在Emacs Lisp中,一个符号可以附加一个值,就像它可以附加一个函数定义一样。这两者是不同的。函数定义是一组计算机会遵从的指令。而值,另一方面,是一些可以变化的东西,比如数字或名称(这就是为什么这样的符号被称为变量)。符号的值可以是Lisp中的任何表达式,比如符号,数字,列表或字符串。一个有值的符号通常被称为变量。
一个符号可以同时附加函数定义和值,或者它可以只有一个。这两者是分开的。这在某种程度上类似于剑桥这个名字可以指向马萨诸塞州的城市,并且名字也可以附加一些信息,比如“很棒的编程中心”。
另一种思考的方式是想象一个符号就像是一个抽屉柜。函数定义放在一个抽屉里,值放在另一个抽屉里,依此类推。放在值抽屉里的东西可以改变,而不会影响放在函数定义抽屉里的内容,反之亦然。
fill-column,一个示例变量
变量 fill-column 说明了一个带有附加值的符号:在每个 GNU Emacs 缓冲区中,这个符号被设置为某个值,通常是 72 或 70,但有时也可以是其他值。要找到这个符号的值,只需单独对其求值即可。如果您在 GNU Emacs 内的 Info 中阅读此内容,可以将光标放在符号后面,然后输入 C-x C-e:
fill-column
在我输入了 C-x C-e 之后,Emacs 在我的回显区打印出了数字 72。这是我在编写时为 fill-column 设置的值。在您的 Info 缓冲区中,可能会有不同的值。请注意,作为一个变量返回的值与执行其指令的函数返回的值完全相同。从 Lisp 解释器的角度来看,返回的值就是返回的值。一旦知道值是什么,它来自何种表达式就不再重要。
一个符号可以绑定到任何值,或者用行话说,我们可以将变量绑定到一个值:比如一个数字,比如 72;一个字符串,"such as this";一个列表,比如 “(spruce pine oak)”;我们甚至可以将变量绑定到一个函数定义。
一个符号可以通过多种方式绑定到一个值。有关执行此操作的一种方法,请参阅“设置变量的值”。
1.7.1 没有函数的符号的错误消息
当我们评估 fill-column 以找出其作为变量的值时,我们没有在这个词周围加括号。这是因为我们不打算将其用作函数名。
如果 fill-column 是列表的第一个或唯一元素,Lisp 解释器会尝试找到与其关联的函数定义。但 fill-column 没有函数定义。尝试评估这个:
(fill-column)
您将创建一个Backtrace缓冲区,上面写着:
---------- 缓冲区:*Backtrace* ----------
调试器进入--Lisp 错误:(void-function fill-column)
(fill-column)
eval((fill-column) nil)
elisp--eval-last-sexp(nil)
eval-last-sexp(nil)
funcall-interactively(eval-last-sexp nil)
call-interactively(eval-last-sexp nil nil)
command-execute(eval-last-sexp)
---------- 缓冲区:*Backtrace* ----------
(记住,要退出调试器并让调试器窗口消失,请在Backtrace缓冲区中键入q。)
1.7.2 没有值的符号的错误消息
如果您尝试评估一个没有绑定值的符号,您将收到一个错误消息。您可以通过尝试我们的2加2加法来体会到这一点。在下面的表达式中,在第一个数字2之前的+之后,输入C-x C-e:
(+ 2 2)
在GNU Emacs 22中,您将创建一个Backtrace缓冲区,其中包含以下内容:
---------- 缓冲区:*Backtrace* ----------
调试器进入--Lisp错误:(void-variable +)
eval(+)
elisp--eval-last-sexp(nil)
eval-last-sexp(nil)
funcall-interactively(eval-last-sexp nil)
call-interactively(eval-last-sexp nil nil)
command-execute(eval-last-sexp)
---------- 缓冲区:*Backtrace* ----------
(您也可以在Backtrace缓冲区输入q来退出调试器。)
此回溯不同于我们看到的第一个错误消息,它说,“调试器进入--Lisp错误:(void-function this)”。在这种情况下,该函数没有值作为变量;而在另一种错误消息中,该函数(单词“this”)没有定义。
在这个对+的实验中,我们是让Lisp解释器评估+并寻求变量的值而不是函数定义。我们通过将光标放在符号的右侧而不是放在封闭列表的括号后面来做到这一点。因此,Lisp解释器评估了前面的s表达式,这种情况下是+本身。
由于+没有绑定值,只有函数定义,错误消息报告说该符号作为变量的值为空。
1.8 参数
为了看看信息是如何传递给函数的,让我们再来看看我们的老朋友,两加二。在Lisp中,这样写:
(+ 2 2)
如果你评估这个表达式,数字4会出现在你的回显区。Lisp解释器所做的就是对+后面的数字进行相加。
由+相加的数字被称为函数+的参数。这些数字是传递给函数的信息。
“参数”这个词来源于它在数学中的用法,并不是指两个人之间的争论;而是指提供给函数的信息,(在本例中就是传递给+的信息)。在Lisp中,一个函数的参数是跟随该函数的原子或列表。这些原子或列表的求值结果被传递给该函数。不同的函数需要不同数量的参数;有些函数根本不需要参数。
脚注(3)
追踪“参数”这个词如何产生数学上和日常英语中有两种不同意义是很奇怪的。根据牛津英语词典资料,这个词源于拉丁语“to make clear, prove”。因此,它通过一个线索的词源,变成“提供作为证据的证据”,也就是说,“提供的信息”,从而导致了它在Lisp中的意义。但在另一个词源线索中,意思变成“以一种反对其他人可以提出反对意见的方式断言”,从而导致了它作为辩论的意思。(请注意这里,英语单词同时附有两个不同的定义。相比之下,在Emacs Lisp中,一个符号不能在同一时间具有两种不同的函数定义。)
1.8.1 参数的数据类型
传递到函数的数据类型取决于它使用的信息类型。例如,函数+的参数必须是数字,因为+是用来相加数字的。其他函数则使用不同类型的数据作为参数。
举个例子,concat函数用来连接两个或多个字符串,产生一个新的字符串。它的参数是字符串。连接两个字符字符串abc和def会产生一个新的字符串abcdef。你可以通过下面的计算来验证:
(concat "abc" "def")
计算这个表达式的结果是"abcdef"。
像substring这样的函数同时使用字符串和数字作为参数。这个函数返回输入字符串的一部分,即第一个参数的子串。这个函数有三个参数,第一个是字符的字符串,第二个和第三个是数字,表示子串的起始(包含)和结束(不包含)位置。这些数字是从字符串的开头计算的字符数量,包括空格和标点符号。注意:字符串中的字符是从零开始编号,而不是从一开始。
比如,如果你计算以下表达式:
(substring "The quick brown fox jumped." 16 19)
你会在显示的区域看到"fox"。这里的参数是字符串和两个数字。
请注意,传递给substring的字符串是一个单一的原子,即使它是由多个用空格分隔的单词组成。Lisp将引号之间的所有内容都视为字符串的一部分,包括空格。你可以将substring函数看作是一种原子粒子对撞机,因为它可以从一个本来无法分割的原子中提取一部分。但是,substring只能从字符串类型的参数中提取子串,而不是从其他类型的原子中,比如数字或符号。
1.8.2 作为变量或列表值的参数
参数可以是一个符号,在求值时返回一个值。例如,当单独求值符号 fill-column 时,它会返回一个数字。这个数字可以在加法中使用。
将光标定位到下面的表达式后,然后输入 C-x C-e:
(+ 2 fill-column)
这个值将会比单独求值 fill-column 得到的值多两。对于我来说,这个值是74,因为我的 fill-column 的值是72。
正如我们刚刚看到的那样,参数可以是一个符号,在求值时返回一个值。此外,参数也可以是一个列表,在求值时返回一个值。例如,在下面的表达式中,函数 concat 的参数是字符串 "The " 和 " red foxes." 以及列表 (number-to-string (+ 2 fill-column))。
(concat "The " (number-to-string (+ 2 fill-column)) " red foxes.")
如果你求值这个表达式——并且,和我的 Emacs 一样,fill-column 求值为72——"The 74 red foxes." 将会出现在回显区。 (请注意,你必须在‘The’这个词后和‘red’这个词前加上空格,这样它们才会出现在最终的字符串中。函数 number-to-string 将加法函数返回的整数转换为字符串。number-to-string 也被称为 int-to-string。)
1.8.3 可变数量的参数
一些函数,比如concat,+或,可以接受任意数量的参数。(是乘法的符号。)通过通常的方式计算下面的表达式,你可以看到在回显区域的输出,它会打印在这段文字后面的“⇒”,你可以理解为“计算结果为”。
在第一组中,这些函数没有参数:
(+) ⇒ 0
(*) ⇒ 1
在这组中,这些函数每个有一个参数:
(+ 3) ⇒ 3
(* 3) ⇒ 3
在这组中,这些函数每个有三个参数:
(+ 3 4 5) ⇒ 12
(* 3 4 5) ⇒ 60
1.8.4 使用错误类型的对象作为参数
当函数传递错误类型的参数时,Lisp解释器会产生一个错误消息。例如,+函数期望其参数的值为数字。作为一个实验,我们可以将引号hello符号传递给它,而不是一个数字。将光标定位在以下表达式之后,然后键入C-x C-e:
(+ 2 'hello)
当你这样做时,会产生一个错误消息。发生的情况是+试图将2添加到'hello返回的值上,但'hello返回的值是符号hello,而不是数字。只有数字可以相加。所以+不能执行它的加法。
你会创建并输入一个Backtrace缓冲区,并说:
---------- Buffer:*Backtrace* ----------
调试器输入--Lisp错误:
(wrong-type-argument number-or-marker-p hello)
+(2 hello)
eval((+ 2 'hello) nil)
elisp--eval-last-sexp(t)
eval-last-sexp(nil)
funcall-interactively(eval-print-last-sexp nil)
call-interactively(eval-print-last-sexp nil nil)
command-execute(eval-print-last-sexp)
---------- Buffer:*Backtrace* ----------
与往常一样,错误消息试图帮助你,并且在学会如何阅读它之后是有意义的。
错误消息的第一部分很简单;它说“错误的类型参数”。接下来是神秘的行话词“number-or-marker-p”。这个词试图告诉你+期望的是什么样的参数。
符号number-or-marker-p说,Lisp解释器正在尝试确定它呈现的信息(参数的值)是数字还是标记(代表缓冲区位置的特殊对象)。它测试看看+是否被赋予要添加的数字。它还测试看看参数是不是叫做标记的东西,这是Emacs Lisp的一个特点。(在Emacs中,缓冲区中的位置被记录为标记。当使用C-@或C-SPC命令设置标记时,它的位置被保持为标记。标记可以被视为一个数字—它是位置距离缓冲区开头的字符数。)在Emacs Lisp中,+可以用来将标记位置的数值加在一起。
number-or-marker-p的“p”是 Lisp 编程早期开始的一种做法。这个“p”代表谓词。在早期Lisp研究人员使用的行话中,谓词指的是用于确定某个属性是真还是假的函数。因此,“p”告诉我们,number-or-marker-p是一个确定所提供的参数是一个数字还是一个标记的函数的名称。以‘p’结尾的其他Lisp符号包括zerop,一个测试它的参数是否为零的函数,和listp,一个测试它的参数是否为列表的函数。
最后,错误消息的最后一部分是符号hello。这是传递给+的参数的值。如果添加的对象类型正确,传递的值将是一个数字,如37,而不是像hello这样的符号。但是这样你就不会得到错误消息了。
注脚(4)
(quote hello)是对缩写'hello的扩展。
1.8.5 消息功能
就像+一样,消息功能接受可变数量的参数。它用于向用户发送消息,非常有用,我们将在此处进行描述。
消息打印在回显区。例如,通过评估以下列表,您可以在回显区中打印消息:
(message "This message appears in the echo area!")
双引号之间的整个字符串是一个单独的参数,并全部打印出来。(请注意,在此示例中,消息本身将以双引号出现在回声区域;这是因为您看到消息功能返回的值。在您编写的程序中的大多数消息使用中,文本将作为副作用在回声区域中打印,而没有引号。有关此的详细信息,请参见multiply-by-seven的示例。)
但是,如果带引号的字符中有一个“%s”,则消息功能不会像此打印“%s”,而是查看接下来的字符串。它计算第二个参数并打印在字符串中“%s”的位置的值。
可以通过将光标定位在以下表达式之后并键入C-x C-e来查看此内容:
(message "The name of this buffer is: %s." (buffer-name))
在“Info”中,“此缓冲区的名称是:* info *。” 将出现在回显区域。 buffer-name函数返回缓冲区的名称作为字符串,并在%s的位置替换消息函数。
要将值作为整数打印,请以与“%s”相同的方式使用“%d”。例如,要在回显区域中打印一条消息,指定fill-column的值,请评估以下内容:
(message "The value of fill-column is %d." fill-column)
在我的系统上,当我评估此列表时,“fill-column的值为72。” 将出现在我的回显区5。
如果带引号的字符串中有多个“%s”,则将打印紧随引号后面的第一个参数的值,并将第二个参数的值打印在第二个“%s”的位置,以此类推。
例如,如果评估以下内容,
(message "There are %d %s in the office!"
(- fill-column 14) "pink elephants")
在回显区中将出现一条颇为奇妙的消息。在我的系统上,它说:“办公室里有58只粉红大象!”。
表达式(- fill-column 14)将被评估,并插入‘%d’的位置。双引号之间的字符串:“粉红象”将被视为一个单独的参数,并插入‘%s’的位置。(也就是说,双引号之间的字符串评估为本身,就像数字一样。)
最后,这是一个稍微复杂的示例,不仅说明了数字的计算,还显示了如何在表达式中使用表达式生成替换‘%s’的文本:
(message "He saw %d %s"
(- fill-column 32)
(concat "red "
(substring
"The quick brown foxes jumped." 16 21)
" leaping."))
在此示例中,消息有三个参数:字符串“他看到了%d%s”,表达式(- fill-column 32),以及以concat函数开头的表达式。评估(- fill-column 32)的结果值插入‘%d’的位置;以及以concat开头的表达式返回的值插入‘%s’的位置。
当您的填充列为70并且评估表达式时,“他看到了38只红狐狸跳跃。” 将出现在回声区域中。
注(5)
实际上,可以使用%s打印数字。它是非特定的。%d只打印小数点左边的部分,并且不打印数字以外的任何内容。
1.9 设置变量的值
变量赋值有几种方法。一种方法是使用特殊形式setq。另一种方法是使用let(参见let)。(这个过程的行话术是将变量绑定到一个值。)
下面不仅描述了setq的工作原理,还说明了如何传递参数。
1.9.1使用setq
要将符号flowers的值设置为列表(玫瑰 紫罗兰 雏菊 金凤花),请在光标放在表达式后面,然后输入C-x C-e来评估以下表达式。
(setq flowers'(rose violet daisy buttercup))
列表(玫瑰 紫罗兰 雏菊 金凤花)将出现在回显区。这就是setq特殊形式返回的内容。作为副作用,符号flowers被绑定到这个列表;也就是说,可以将视为变量的符号flowers赋予列表作为其值。(顺便说一句,这个过程说明了向Lisp解释器产生的副作用,设置该值可能是我们人类感兴趣的主要效果。这是因为每个Lisp函数在没有出错时必须返回一个值,但如果不指定,它将只产生一个副作用。)
在评估setq表达式后,可以评估符号flowers,并返回所设置的值。这里是这个符号。把光标放在它后面,然后输入C-x C-e。
flowers
当你评估flowers时,列表(玫瑰 紫罗兰 雏菊 金凤花)将出现在回显区。
顺便说一下,如果你评估'flowers,即在变量前面加上引号,你将在回显区看到符号本身,flowers。这里是带引号的符号,所以你可以试一下:
'flowers
另外,作为额外方便,setq允许你在一个表达式中为多个不同的变量设置不同的值。
要使用setq将变量carnivores的值设置为列表'(狮子 老虎 豹),使用以下表达式:
(setq carnivores'(lion tiger leopard))
此外,setq还可用于将不同的值分配给不同的变量。第一个参数绑定到第二个参数的值,第三个参数绑定到第四个参数的值,以此类推。例如,你可以使用以下语句将树的列表分配给符号trees,将食草动物的列表分配给符号herbivores:
(setq trees '(pine fir oak maple)
herbivores '(gazelle antelope zebra))
(这个表达式也可以写在一行上,但可能一页没有显示完;而人类发现漂亮格式化的列表更容易阅读。)
虽然我一直在使用“分配”这个词,但有另一种思考setq工作原理的方式;那就是说setq让符号指向列表。这种思考方式非常常见,在接下来的章节中我们将至少遇到一个具有“指针”作为其名称的符号。这个名称是选择的,因为符号有一个值,特别是一个列表,附加到它上面;或者,用另一种方式来表达,符号被设置为指向列表。
1.10 总结
学习Lisp就像爬山,一开始是最陡的部分。你已经完成了最困难的部分;随着你继续前进,剩下的部分会变得更容易。
总之,
- Lisp程序由表达式组成,这些表达式是列表或单个原子。
- 列表由零个或多个原子或内部列表组成,由空格分隔并由括号括起来。列表可以为空。
- 原子是多字符符号,比如forward-paragraph,单字符符号如+,双引号之间的字符字符串,或数字。
- 数字的值等于它本身。
- 双引号之间的字符串也等于它本身。
- 当你单独评估一个符号时,它的值会被返回。
- 当你评估一个列表时,Lisp解释器会查看列表中的第一个符号,然后查看绑定到该符号的函数定义。然后,函数定义中的指令将被执行。
- 单引号'告诉Lisp解释器,它应该按照写的方式返回以下表达式,并不像如果没有引号那样进行评估。
- 参数是传递给函数的信息。函数的参数是通过评估函数为第一个元素的列表的其余元素来计算的。
- 当函数被评估时,它总是会返回一个值(除非它出现错误);此外,它还可能执行一些作为副作用的操作。在许多情况下,函数的主要目的是创建一个副作用。
1.11 练习
一些简单的练习:
- 通过评估不在括号内的适当符号生成错误消息。
- 通过评估在括号之间的适当符号生成错误消息。
- 创建一个每次增加两个而不是一个的计数器。
- 编写一个在评估时在回显区域打印消息的表达式。
浙公网安备 33010602011771号