2.4 序对、列表和 Racket 语法
cons 函数实际上接受两个任意值,而不止是一个列表作为第二个参数。 当第二个参数非 empty 且自身不是由 cons 产生时, 其结果会打印为特殊的形式。由 cons 结合的两个值会打印在括号中, 不过它们之间会有个点(即一个两侧为空格的英文句点):
> (cons 1 2) '(1 . 2)
> (cons "banana" "split") '("banana" . "split")
因此,由 cons 产生的值并不总是列表。通常,cons 的结果是一个 序对。cons? 函数更传统的名字是 pair?, 我们之后会使用这个传统的名字。
rest 对非列表的序对也没什么意义。first 和 rest 更传统的名字分别是 car 和 cdr。(不过,传统的名字也没什么意义。 你只要记住“a”在“d”前面,cdr发音为“could-er”就好了。)
> (car (cons 1 2)) 1
> (cdr (cons 1 2)) 2
> (pair? empty) #f
> (pair? (cons 1 2)) #t
> (pair? (list 1 2 3)) #t
Racket 的序对数据类型和它与列表的关系,连同打印时的点号记法和滑稽的名字 car 和 cdr 基本上都是历史的奇妙产物。 序对深深地刻印在了 Racket 的文化、规范和实现中,它们因而在语言中得以留存。
你很可能因为非列表序对而犯错,例如不小心弄反了 cons 的参数:
> (cons (list 2 3) 1) '((2 3) . 1)
> (cons 1 (list 2 3)) '(1 2 3)
有时我们需要特意去使用非列表序对。例如,make-hash 函数接受一个序对的列表, 其中每个序对的 car 为键,cdr 为值。
比非列表序对还要让 Racket 新手感到困惑的是另一种序对的打印约定,其第二个元素为 序对而非列表:
> (cons 0 (cons 1 2)) '(0 1 . 2)
通常,序对的打印遵循如下规则:使用点号记法,除非点号后面紧跟着开括号, 而在这种情况下,点号、紧跟的开括号以及匹配的闭括号会被省略。因此 '(0 . (1 . 2)) 会打印为 '(0 1 . 2), 而 '(1 . (2 . (3 . ()))) 则会打印为 '(1 2 3)。
2.4.1 用 quote 来引述序对和符号
列表在打印时会在前面标一个单引号。但如果列表的元素本身也是列表,那么内部的列表前面则不会有单引号:
> (list (list 1) (list 2 3) (list 4)) '((1) (2 3) (4))
特别来说,对于嵌套列表而言,qoute 形式能让你将列表写成表达式, 其形式基本上与列表的打印形式相同:
> (quote ("red" "green" "blue")) '("red" "green" "blue")
> (quote ((1) (2 3) (4))) '((1) (2 3) (4))
> (quote ()) '()
无论列表的引述形式是否会被点号-括号消除规则正规化,qoute 形式都可以与点号形式配合使用:
> (quote (1 . 2)) '(1 . 2)
> (quote (0 . (1 . 2))) '(0 1 . 2)
自然,任何种类的列表均可以嵌套:
> (list (list 1 2 3) 5 (list "a" "b" "c")) '((1 2 3) 5 ("a" "b" "c"))
> (quote ((1 2 3) 5 ("a" "b" "c"))) '((1 2 3) 5 ("a" "b" "c"))
如果你用 quote 包裹了标识符,那么其输出形式类似于带有 ' 前缀的标识符:
> (quote jane-doe) 'jane-doe
打印形式为带有单引号前缀的标识符的值,叫做 符号。 同带括号的输出不应与表达式混淆一样,符号的打印也不应当与标识符混淆。 具体来说,除了字母相同外,符号 (quote map) 同标识符 map 或绑定到 map 的预定义函数之间没有任何关系。
实际上,符号固有的值除了其字符常量外再无其它。从这个意义上来说,符号和字符串几乎是一样的, 而它们的主要区别就是打印的形式。函数 symbol->string 和 string->symbol 可以在二者之间互相转换。
> map #<procedure:map>
> (quote map) 'map
> (symbol? (quote map)) #t
> (symbol? map) #f
> (procedure? map) #t
> (string->symbol "map") 'map
> (symbol->string (quote map)) "map"
同 quote 会自动应用到嵌套的列表内一样,对于括号括住的标识符序列来说, 它也会自动应用到其中的标识符上来创建出符号列表:
> (car (quote (road map))) 'road
> (symbol? (car (quote (road map)))) #t
当符号在以 ' 打印的列表中时,符号前的 ' 会被省略, 因为 ' 已经自动做了这件事:
> (quote (road map)) '(road map)
quote 形式对数值或字符串之类的字面量表达式没有效果:
> (quote 42) 42
> (quote "on the record") "on the record"
2.4.2 将 quote 简写为 '
如你所料,你可以将 quote 简写为将 ' 放在表达式前面来引述它:
> '(1 2 3) '(1 2 3)
> 'road 'road
> '((1 2 3) road ("a" "b" "c")) '((1 2 3) road ("a" "b" "c"))
在文档中,表达式中的 ' 及其后面的形式会打印为绿色,因为这种组合其实初春贯彻常量表达式。 在 DrRacket 中,只有 ' 会显示为绿色。DrRacket 要更加精准正确,因为 quote 的意思会随表达式的上下文而不同。然而在文档中,我们通常假定标准的绑定是在作用域内的, 因此为了更加清楚,我们就把引述的形式渲染成了绿色。
' 会按其字面意思展开为 quote 形式。如果你在带有 ' 的形式前面再加一个 ',就会看到这一点:
> (car ''road) 'quote
> (car '(quote road)) 'quote
' 简写在输出时的行为与输入时一样。REPL 的打印器在打印输出时, 会将符号 'quote 识别为一个两元素列表的第一个元素,此时它会使用 ’ 来打印输出:
> (quote (quote road)) ''road
> '(quote road) ''road
> ''road ''road
2.4.3 列表与 Racket 语法
现在你已经知道了序对和列表的真相,也见过了 quote, 你已经准备好理解我们简化真正的 Racket 语法的主要方式了。
Racket 的语法并不是直接根据字符流来定义的,而是由两个层次来确定的:
打印和读取的规则是相辅相成的。例如,列表会打印为带括号的形式, 而读取一对括号会产生一个列表。同样,非列表序对会打印为点号记法, 而输入点号实际上会反向执行点号记法的规则,从而获取一个序对。
读取层作用于表达式的一个结果是,你可以在非引述形式的表达式中使用点号记法:
> (+ 1 . (2)) 3
这样可行是因为 (+ 1 . (2)) 不过是 (+ 1 2) 的另一种写法。在实践中,使用这种点号记法来编写应用表达式绝对不是个好主意, 这只是 Racket 语法定义方式的一个结果而已。
通常,在括号括住的序列中,读取器只允许在最后一个元素之前使用 .。 然而,在括号括住的序列中,一对 . 也可以出现在单个元素两侧, 只要该元素不是第一个或最后一个几个。这种用法会触发读取器进行转换, 将两个 . 中间的元素移到列表的最前面。这种转换赋予了我们使用通用的中缀记法的能力:
> (1 . < . 2) #t
> '(1 . < . 2) '(< 1 2)
这种两个点号的约定并不是传统的,它与非列表序对中的点号记法实际上也没什么关系。
Racket 程序员对中缀约定的使用非常保守—