补充的 Lisp 知识
1 说明
SICP 原书刻意避开了一些在实际使用 Lisp 时需要了解的知识。
此外,经过了几十年,人们使用 Lisp 时的习惯也在演化。
同时,本项目使用的 Racket 编程语言和 Scheme 有一些差异。
本页面会补充这些知识。
2 数值精度问题
Racket 和 Chez Scheme 都支持大整数类型,所以不必担心整数溢出等问题。例如,可以轻易计算出 2^{256} 的值:
> (expt 2 256) 115792089237316195423570985008687907853269984665640564039457584007913129639936
在 Racket 中,整数除法 (/ a b) 的结果类型为有理数(或整数,如果分母为 1),不会进行浮点除法或整除。
> (/ 6 10) 3/5
> (/ 6 2) 3
根据 Scheme 标准 IEEE 1178-1990 ,符合标准的 Scheme 实现并不一定要支持大整数和有理数类型。Racket 和 Chez Scheme 对它们的支持,都属于扩展。
3 方括号
IEEE Scheme 标准是比较早的文献,没有规定方括号和花括号的用途,只说了将它们“保留,用于未来可能的语言扩展”(reserved for possible future extensions to the language),标准全文都只用圆括号写代码。同时代的 SICP,也是全书都使用圆括号写代码。
但现在在 Scheme 和 Racket 编程中,人们有时会使用方括号,例如:
> (define (fib n) (cond [(= n 0) 0] [(= n 1) 1] [else (+ (fib (- n 1)) (fib (- n 2)))])) > (map fib '(0 1 2 3 4 5 6)) '(0 1 1 2 3 5 8)
事实上,在 Scheme 和 Racket 中,现在一般都将方括号和圆括号视为等价物,可以随意互换。(但至于 ([)] 这种诡异的结构还是算了吧。)这在其他 Lisp 方言中不一定成立,例如 Clojure 用方括号表示向量。
所以方括号一般要在什么地方用呢?
方括号一般在一些特殊形式里,标记会在一个表内多次出现的、本身长度固定的列表,它们一般都是子句。
也就是说,如果我们写代码时,一些代码因为语法规定会长成这样:
((a-1 b-1) |
(a-2 b-2) |
... |
(a-n b-n)) |
其中所有的 a-i 都有着类似的功能,所有的 b-i 都有着类似的功能,那么这时候用方括号就很合适了:
([a-1 b-1] |
[a-2 b-2] |
... |
[a-n b-n]) |
除了刚才的 cond 之外,再举一个 let 的例子:
> (let ([a 1] [b 2] [c 3]) (+ a b c)) 6
刚才的例子都是表长固定为 2 的情况。也有表长为 3 的情况,例如 do 中描述各个变量更新的代码(下面这段示例代码来自 IEEE Scheme 标准,做了修改):
> (define x '(1 3 5 7 9))
> (do ([x x (cdr x)] [sum 0 (+ sum (car x))]) ((null? x) sum)) 25
Racket 官方文档对 cond 的讲解 中提到,这是一种惯例写法,这样做有助于提高可读性。
在 Racket 中,方括号还是比较常见的,会出现在包括但不限于:
这些特殊形式里。
4 宏
SICP 中没有涉及 Lisp 宏,只在 4.1.2 节中的一个脚注里提了一句。
应当指出,Lisp 最精髓的地方不是发明了 REPL,甚至也不是发明了 GC(垃圾回收),而是成为了一个“手写语法树”的语言,让代码和数据共享同一个表示方式。这样一来,就能够轻易编写出宏,使用代码对代码作出变换。 对普通数据(表)做处理、变换的代码,可以无缝地对代码做处理、变换。 这是其他语言极难做到的,除非其他语言也将自己变成手写语法树的样子。Racket 社区的新项目 Rhombus 语言 则在尝试另辟蹊径,在避免传统 Lisp 大量括号、使用一套更“正常”的语法的同时,保证语言的可扩展性。
自然,学习 SICP 并不需要理解宏,因为全书都没有使用过它。但在学习之余,也可以试着了解一下有关 Lisp 宏的知识。
第三章中,延迟求值的语法就可以使用宏实现。
5 表,不适当表,表结构,空表,序对
5.1 不适当表
在书中,我们知道解释器会将 (cons 1 (cons 2 (cons 3 '()))) 打印成 (1 2 3) ,如下:
> (cons 1 (cons 2 (cons 3 '()))) '(1 2 3)
但书中没有提到, (cons 1 (cons 2 3)) 会打印成什么样。事实上,会像这样打印:
> (cons 1 (cons 2 3)) '(1 2 . 3)
可以看到,对于最后的那个序对,解释器将它的两个元素都打印出来,但中间会加上一个点号,表示“这个序对链没有以 nil 结尾”。
像这样,对于一个序对,如果一直对其应用 cdr 直至它不是序对,最后得到的结果却不是 nil ,那么这样的对象称为“不适当表”(improper list)。原本就不是序对的对象,并不属于不适当表,空表 nil 也是如此。
根据上述说法,我们可以写一个过程 improper-list? 来检查一个对象是否是“不适当表”:
> (define (improper-list? x) (define (check p) (if (pair? p) (check (cdr p)) (not (null? p)))) (if (pair? x) (check x) false))
> (map improper-list? (list (cons 1 (cons 2 '())) (cons 2 '()) '() (cons 1 (cons 2 3)) (cons 2 3) 3)) '(#f #f #f #t #t #f)
利用 list? 谓词, improper-list? 可以实现得更简单:
> (define (improper-list? x) (and (pair? x) (not (list? x))))
> (map improper-list? (list (cons 1 (cons 2 '())) (cons 2 '()) '() (cons 1 (cons 2 3)) (cons 2 3) 3)) '(#f #f #f #t #t #f)
(上述代码并没有考虑循环列表。循环列表在原书练习题 3.13、3.18、3.19 中有提及,在本页中的 序对的可变性 部分也有讲解。)
可以发现,普通的序对也是不适当表,所以我们可以知道解释器是怎样打印一个普通序对的了:
> (cons 1 2) '(1 . 2)
对称地,我们也就有了一种不用 cons 就表达一个序对或者不适当表的方式:
> '(1 . 2) '(1 . 2)
> '(1 2 . 3) '(1 2 . 3)
(SICP 竟然在全书都避免了打印不适当表。)
5.2 空表是表吗
书中回避了对“ nil 是不是表”的讨论,见原书 2.2.1 节脚注。但还是 2.2.1 节,另一个脚注说:“在这本书里,我们用术语 表 (list)专指那些有表尾结束标记的序对的链。与此相对应,用术语 表结构 (list structure)指所有的由序对构造起来的数据结构,而不仅是表。”
这句话说表是“序对的链”,所以一个对象想要是表的话似乎至少应该存在一个序对?所以 nil 或者说 '() 就应该不是表了?但事实上,Lisp 程序员一般都会认可“空表也是一个表”这种说法,因为这样会有更好的性质。标准 Common Lisp、标准 Scheme 和 Emacs Lisp 等方言都是这样的。脚注里直接描述成了“序对的链”应该只是编者的疏忽,不必咬文嚼字。
在 Racket 里试一下:
> (list? '()) #t
所以可以对“表结构”的定义做一个修正:空表 nil 也认为是表结构。于是我们可以写一个过程 list-structure? 判断一个对象是不是表结构:
> (define (list-structure? x) (or (null? x) (pair? x)))
> (map list-structure? (list (cons 2 '()) '() (cons 2 3) 3)) '(#t #t #t #f)
我们还可以发现,这个修正过后的“表结构”其实刚好就是“适当表”和“不适当表”的合称。所以还能改写 list-structure? :
> (define (list-structure? x) (or (list? x) (improper-list? x)))
> (map list-structure? (list (cons 2 '()) '() (cons 2 3) 3)) '(#t #t #t #f)
Reddit 上有一个帖子:“Why does nil have to be both an atom and a list?”上面的讨论值得一读。帖子里的楼主认为,空表是一个表却不是序对,有些奇怪。网友讲解为什么这是合理的。
(注意:这个版面不止会讨论 Scheme,还会讨论 Lisp 的其他方言,注意不同方言的不同之处。例如在 Common Lisp 中, nil 是一个特殊符号,正如 2.1.1 节脚注所说。Common Lisp 还用 nil 表示假值,它不能通过 if 测试,而在 Scheme 中并非如此,诸如此类。)
此外,Racket 其实也用一个内置普通变量表示空表,但它叫 null ,而非 nil 。
> null '()
5.3 序对的可变性
TODO:循环表,以及不同实现的解释器如何打印它们;Racket 序对的不可变性以及 list? 结果被缓存