补充的 Lisp 知识
1 说明
2 数值精度问题
3 方括号
4 
5 表,不适当表,表结构,空表,序对
5.1 不适当表
5.2 空表是表吗
5.3 序对的可变性
8.16

补充的 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? 结果被缓存