S表达式,是Lisp语言的特色,
让我们有能力对程序和数据进行统一编码。
这种一致性,
使得我们可以和处理数据一样处理Lisp代码,
正因为这样,才可以建立其他语言无法比拟的宏系统(macro system)。
S表达式真是天才之作呀。
它是凭空想出来的吗?
不是的。
S表达式是二叉树的一种线性编码。
二叉树
我们知道二叉树是很重要的数据结构,
可以用来存储结构化的数据。
例如:
1 | * |
二叉树的每个节点,
或者是叶节点,或者有2个子节点,
叶节点可以用来存储数据。
可是,这样表示二叉树,太麻烦了。
不容易书写。
于是,先哲们发明了点对表示法,
((a . b) . (c . d))
可以表示上面的二叉树,
其中“.
”表示节点。
S表达式是点对表示法的形式定义:
1 | 原子 -> 数字 | 符号 |
所以,S表达式或者是原子,
或者是递归的由其他S表达式构成的点对。
S表达式的化简
实际使用时,
书写S表达式,还要同时写很多点号“.
”,
这也是一件麻烦的事情。
因此,Lisp语言定义了一套S表达式的化简规则。
(1)如果一个点号右邻一个左括号,那么就可以将这个点号,左括号以及匹配的右括号,一起去掉。
例如:(a . (b . c)) <=> (a b . c)
(2)如果一个点号右邻原子nil,那么就可以把这个点号和原子nil
,一起去掉。
例如:(a . (b . nil)) <=> (a b . nil) <=> (a b)
列表是一种特殊类型的S表达式,
如果一个S表达式不是原子,
而且经过化简可以把点号都去掉,
就说这个S表达式是一个列表。
例如:(a . (b . (c . nil))) <=> (a b c)
是一个列表。
为了使用方便,还允许有空表,
而且还定义空表等价于原子nil
。
() <=> nil
例如:(a . (b . ())) <=> (a . (b . nil)) <=> (a b)
值得注意的是,
空表的记法,使nil
身兼三职,
(1)nil
表示一个原子
(2)nil
表示空表
(3)nil
表示逻辑值“假”
nil
成为了Lisp语言中唯一的既是原子又是列表的表达式。
常用函数pair?,cons和car
Scheme语言是Lisp的一种方言,
它没有nil
的概念,只有空表()
。
在介绍常用函数之前,
我们需要知道,Scheme语言的求值策略(evaluation strategy)。
Scheme语言中的函数是按值调用的(call by value),
在求值函数体之前,实参会先被求值。
因此,(display (* 2 3))
中,
(* 2 3)
并不会被看成是具有3个元素的列表,而是看成乘法函数调用,
在display
调用之前,会先求值(* 2 3) => 6
,结果是显示6
。
假如,我们一定要把(* 2 3)
看成是列表呢?
就需要引用它。(display '(* 2 3))
此时,'(* 2 3)
求值为列表(* 2 3)
。
结果是显示(* 2 3)
。
我们来看pair?
函数,
它只有在参数是点对的情况下返回#t
,其余都返回#f
。
例如:
1 | (pair? '(a . c)) => #t |
值得注意的是,
(1)空表不是点对。
因为它是原子nil
,虽然Scheme语言中没有内置原子nil
。
可以通过另外一个函数看出来,atom?
用来判断参数是否原子。
(atom? '()) => #t
(2)列表是点对的简写。
(a b c) <=> (a . (b . (c . ())))
我们再来看cons
函数,
cons
接受两个参数,将他们拼成一个点对,
结果是化简后的S表达式。
例如:
1 | (cons 'a 'b) => (a . b) |
最后来看一下car
和cdr
函数,
它们接受一个点对作为参数,
car
获取点对的左元素,cdr
获取右元素。
1 | (car '(a . b)) => a |
结语
刚学Lisp语言的时候,
查看了很多书,对S表达式的解释,总是不够清楚。
无意间找到了马希文老师的《LISP语言》,
书中用中国人的方式写了对Lisp语言的理解,
言简意赅。
让我认识到了,
真正精髓的知识,都一定是简单的,
难以理解的东西,可能作者也没有搞明白。