Lispemacs lisp语言介绍

[Emacs] Emacs之魂(三):列表,引用和求值策略

2017-05-19  本文已影响509人  何幻

回顾

上文我们介绍了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语言中一种常用的数据结构,用来表示一批数据。
例如,由整数123构成的列表对象,Lisp会将它打印为,(1 2 3)
各个列表元素用空格分隔,用圆括号括起来。

然而,在Lisp代码中直接写(1 2 3),并不会创建一个列表对象,
因为Lisp程序也是用括号方式表示的,例如,(+ 1 2)表示对整数12进行加法运算。

那么如何才能创建一个列表对象呢?
我们需要调用list函数,(list 1 2 3),这段代码将会创建一个由整数123构成的列表对象,
这个列表对象打印为(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属性值是1y属性值是一个数组。
字面量表示法,使得我们不必调用new Objectnew 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)都首先要被求值,分别求值为37
然后再进行乘法运算,结果为21

而宏和内置的特殊命令,并不要求如此,它们有自己的对参数的求值策略。

其中“1”称之为自求值对象,对它进行求值将得到它本身,

1
(eval 1)
(eval (eval 1))

其结果都为1,其它的自求值对象还包括布尔值,字符串,向量,等等。
(+ 1 2)12之前没有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

上一篇下一篇

猜你喜欢

热点阅读