2.4 Pairs, Lists, and Racket Syntax
cons 函数实际上接受任意两个值,而不仅仅是列表作为第二个参数。当第二个参数不是 empty 且不是由 cons 本身产生的时候,结果会以特殊的方式打印。用 cons 连接的两个值打印在括号之间,中间用一个点(即被空白包围的句号)分隔:
> (cons 1 2) '(1 . 2)
> (cons "banana" "split") '("banana" . "split")
因此,由 cons 产生的值并不总是列表。一般来说,cons 的结果是一个pair。cons? 函数更传统的名称是 pair?,从现在起我们将使用传统名称。
对于非列表 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 的 pair 数据类型及其与列表的关系本质上是一种历史遗留,包括用于打印的点记法和奇怪的名称 car 和 cdr。然而,pair 深深嵌入了 Racket 的文化、规范和实现中,因此它们在语言中得以保留。
你最可能在犯错误时遇到非列表 pair,比如意外地反转了 cons 的参数:
> (cons (list 2 3) 1) '((2 3) . 1)
> (cons 1 (list 2 3)) '(1 2 3)
非列表 pair 有时会被有意使用。例如,make-hash 函数接受一个 pair 列表,其中每个 pair 的 car 是键,cdr 是任意值。
对新手来说,比非列表 pair 更令人困惑的是第二个元素是 pair 但不是列表的 pair 的打印约定:
> (cons 0 (cons 1 2)) '(0 1 . 2)
一般来说,pair 的打印规则如下:使用点记法,除非点后面紧跟一个左括号。在这种情况下,删除点、左括号和匹配的右括号。因此,'(0 . (1 . 2)) 变成 '(0 1 . 2),'(1 . (2 . (3 . ()))) 变成 '(1 2 3)。
2.4.1 Quoting Pairs and Symbols with quote
列表打印时前面有一个引号,但如果列表的元素本身是列表,则内层列表不会打印引号:
> (list (list 1) (list 2 3) (list 4)) '((1) (2 3) (4))
特别是对于嵌套列表,quote 形式让你可以按照列表打印的方式将列表写成表达式:
> (quote ("red" "green" "blue")) '("red" "green" "blue")
> (quote ((1) (2 3) (4))) '((1) (2 3) (4))
> (quote ()) '()
quote 形式也适用于点记法,无论引用的形式是否经过点-括号消除规则的规范化:
> (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 会自动应用于嵌套列表一样,对括号包围的标识符序列使用 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 Abbreviating quote with '
你可能已经猜到,你可以在要引用的形式前面放 ' 来缩写 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 Lists and Racket Syntax
既然你已经了解了 pair 和列表的真相,并且已经见过了 quote,你已经准备好理解我们一直在简化 Racket 真正语法的主要方式。
Racket 的语法不是直接根据字符流定义的。相反,语法由两层决定:
打印和读取的规则是相辅相成的。例如,列表用括号打印,读取一对括号会产生列表。类似地,非列表 pair 用点记法打印,输入中的点有效地反向执行点记法规则以获得 pair。
读取层对表达式的一个后果是你可以在非引用形式的表达式中使用点记法:
> (+ 1 . (2)) 3
这之所以有效,是因为 (+ 1 . (2)) 只是 (+ 1 2) 的另一种写法。使用这种点记法来编写应用表达式实际上从来不是好主意;这只是 Racket 语法定义方式的结果。
通常,读取器只允许在括号序列中使用 .,且只在序列的最后一个元素之前。然而,一对 . 也可以出现在括号序列中的单个元素周围,只要该元素不是第一个或最后一个。这样的配对会触发读取器转换,将 . 之间的元素移到列表的前面。这种转换启用了一种通用的中缀记法:
> (1 . < . 2) #t
> '(1 . < . 2) '(< 1 2)
这种两点约定是非传统的,与非列表 pair 的点记法基本上没有关系。Racket 程序员很少使用中缀约定——主要用于不对称的二元运算符,如 < 和 is-a?。