回顾
上文我们介绍了Emacs的用法,发现一分钟学会使用它并不是难事,
而且,我们没有让快捷键束缚住,因为Emacs的精髓在于Emacs Lisp中。
本文我们开始探讨Emacs Lisp,不过在这之前我们还要先熟悉一下Lisp的特点和Lisp家族的成员,
随后本文重点分析和介绍了列表,引用和求值策略,
这几个概念,尤其是引用,对学习者来说非常容易引起困惑,
本文采用了不同的角度来描述这些概念。
1. 强大的Lisp
1960年,John McCarthy发表了一篇计算机领域的文章,这是一篇“惊世之作”,
它的作用简直就像欧几里得《几何原本》对几何学的贡献一样。
John McCarthy只用了一些简单的的运算符和函数,构建出了一门图灵完备的编程语言,
称之为Lisp,Lisp是列表处理(List Processing)的简称。
这门语言的关键思想是,不论代码还是数据,都用统一的数据结构(列表)进行表示。
Lisp语言具有很强的表达能力,我们可以用更少的代码做更多的事情。
通常而言,语言具有表达能力就必须提供丰富的内置功能和强大的扩展性。
语言的内置功能指的是语言默认提供的功能,它能减少程序员的重复劳动,帮助他们快速完成工作。
语言的扩展性,指的是当语言内置功能不能满足需要的时候,程序员可以怎样做。
同时具有丰富的功能和强大的扩展性是很困难的,这需要在语言的设计阶段就考虑好,
语言的内置功能越多,就会越复杂,扩展功能的与内置功能的一致性就很难被保证。
现代的高级编程语言,离不开编译器和解释器,
编译器将高级语言的代码转换成更底层的语言,例如C语言或者汇编,
解释器提供了一个运行时环境,直接解释执行高级语言的源代码。
一般而言,编译器是由语言的开发商提供的,使用者并不会参与到编译器的开发工作之中,
如果想要在语言中支持一等函数(first-class function),就必须让语言的开发商改写编译器,
如果需要增加新的类似if-else的控制结构,或者让语言支持面向对象编程,也要改写编译器才行。
因此,语言支持什么功能,以及源代码被如何编译,完全取决于开发商。
而Lisp语言则不同,它允许程序员对编译器进行编程,(元编程
Lisp程序员可以决定代码被如何编译甚至如何被读取,像是半个编译器的开发者一样。
2. Lisp-1和Lisp-2
Lisp语言构成了一个家族,具有成百上千种方言,
用的最多的几种是,Common Lisp,Scheme,Racket,和Emacs Lisp。
其中Scheme的目标是简洁,Common Lisp提供了强大的工业级支持,
Racket提供了一种创造语言和设计实践的平台,Emacs Lisp主要用于Emacs中。
Emacs Lisp更像Common Lisp,它们都是Lisp-2,
即同一个符号在不同的上下文中,可以分别用来表示变量和函数,
而Scheme和Racket则只能用来表示同一个实体,称为Lisp-1。
出现Lisp-2,主要是因为有效率方面的考虑,
在Lisp-2中,函数和变量分属不同的名字空间,在不同的环境中,由不同的求值器进行处理。
这样做也使语义更加复杂了,以后的文章中,我们会介绍Emacs Lisp中符号(Symbol)的概念。
3. 列表对象和它的文本表示
列表是Lisp语言中一种常用的数据结构,用来表示一批数据。
例如,由整数1
,2
和3
构成的列表对象,Lisp会将它打印为,(1 2 3)
。
各个列表元素用空格分隔,用圆括号括起来。
然而,在Lisp代码中直接写(1 2 3)
,并不会创建一个列表对象,
因为Lisp程序也是用括号方式表示的,例如,(+ 1 2)
表示对整数1
和2
进行加法运算。
那么如何才能创建一个列表对象呢?
我们需要调用list
函数,(list 1 2 3)
,这段代码将会创建一个由整数1
,2
和3
构成的列表对象,
这个列表对象打印为(1 2 3)
。
注意,以上我们严谨的区分了Lisp对象和它的打印结果,
是因为对象和它的文本表示(textual representation)是不同的概念。
例如,在C语言中我们写,
int result = 1 + 2;
我们实际上是用“1”表示了整数1
,“1”只是一段文本,是印刷符号,而整数1
是一个数学对象,
同样的,“+”是一段文本,它表示了加法运算符。
在Lisp语言中,我们用文本来写程序,而Lisp读取器得到的是Lisp对象,
经过对这些Lisp对象进行计算,得到了计算结果,也是一个Lisp对象,
最终,反馈给我们的是这个对象的文本表示。
4. 字面量和引用
在Lisp中,我们用文本“1”可以直接表示整数1
,用“#t”表示真值,
类似的“1”和“#t”,称之为对象的字面量表示(literal representation)。
其它语言中,也提供了广泛的字面量表示法,例如,JavaScript提供了数组和对象字面量,
const obj = {
x: 1,
y: [2, 3, 4]
};
这段代码创建了一个JavaScript Object
,它的x
属性值是1
,y
属性值是一个数组。
字面量表示法,使得我们不必调用new Object
和new Array
来创建它。
Lisp中列表对象用的非常多,每次都使用list
函数来创建是一件麻烦的事情,
因此,Lisp语言提供了列表对象的字面量写法,我们只需要调用quote
就可以了。
(quote (1 2 3))
以上Lisp代码会创建一个打印形式为(1 2 3)
的列表对象。
对于嵌套列表,使用quote
是非常方便的,
(quote (1 (2 3) ((4 5) (6 7))))
像这样创建列表的方式,称为引用(quoting),
这不同于按引用调用(call by reference)中的“引用”(reference)。
quote
还有一个便捷的写法,就是用单引号来表示它,(quote (1 2 3))
可以表示为,
'(1 2 3)
我们只需要在列表前加一个单引号即可,因为列表的右括号表明了它在引用这个列表。
5. quote和list
值得一提的是,引用并不保证每次都会重新创建列表。
例如,在Lisp中我们使用define
创建函数,
(define (foo)
'(1 2 3))
然后,用以下方式进行函数调用,注意foo
参数个数为0个,
(foo)
多次调用foo
,编译器可能返回同一个列表对象。
而list
则不同,每次调用它会返回一个全新的列表对象,
(define (foo)
(list 1 2 3))
6. 求值策略
Lisp代码是由表达式构成的,Lisp程序的执行过程就是表达式的求值过程,
(* (+ 1 2) (+ 3 4))
以上表达式的求值结果为21
。
在程序的列表表示法中,从左到右位于第一个位置的元素,是比较特殊的,
它表示一个函数(function),一个宏(macro),或者一个内置的特殊命令(special form)。
位于其他位置的元素称为参数。
函数被调用的时候,它的每个参数都必须首先被求值,
例如,以上程序中*
,+
都是函数,
在调用乘法函数*
时,它的参数(+ 1 2)
和(+ 3 4)
都首先要被求值,分别求值为3
和7
,
然后再进行乘法运算,结果为21
。
而宏和内置的特殊命令,并不要求如此,它们有自己的对参数的求值策略。
其中“1”称之为自求值对象,对它进行求值将得到它本身,
1
(eval 1)
(eval (eval 1))
其结果都为1
,其它的自求值对象还包括布尔值,字符串,向量,等等。
(+ 1 2)
中1
和2
之前没有quote
,是因为它们是自求值对象,(+ '1 '2)
和(+ 1 2)
的计算结果是相等的。
7. 在Emacs中求值表达式
Emacs可以直接求值文本中的Lisp代码,我们只需要将光标定位到列表尾部,
然后按快捷键C-x C-e
即可。(指的是按Ctrl+x
,然后再按Ctrl+e
我们还可以试试quote
和自求值对象,1
求值为1
,'1
求值为1
。
然而''1
却求值为(quote 1)
,是因为''1
实际是(quote (quote 1))
,
它表示用字面量方式创建了一个形如(quote 1)
的列表对象。
下文,我们来讨论Emacs Lisp的控制结构和基本的数据类型,
使用Lisp编程是一件有趣的事情。
参考
The Roots of Lisp
Land of Lisp
Lisp in small pieces
An Introduction to Programming in Emacs Lisp
GNU Emacs Lisp Reference Manual