Scheme 提供一些简单的过程用于输入/输出操作。表达式可以装载自文件,并在这个过程期间发生求值。在必须用户交互的编程任务中可以使用 read 函数。display 将打印任何对象的值,在需要换行(line-feed)的地方可以使用换行符(newline)。 (define LuckyNumber (read)) ; 键入 7
; LuckyNumber
LuckyNumber
; 7
(display LuckyNumber)
; 7 #t
响应最后一个表达式的 #t 是 display 返回的值。"7" 的打印作为副作用发生。尽管不是标准特征,我们将使用两个额外的打印过程来试图精简与我们的程序输出有关的大批代码。DisplayList 和 DisplayLine 接受任意数目的参数。DisplayLine 还在结束处进行一次换行。这两个过程都是系统工具箱的一部分。 (DisplayLine LuckyNumber "is not" 13)
; 7 is not 13 #t
Scheme 支持三种不同的对等价的测试,它们可以应用于任何对象。eq? 定义等价的最强解释。它在 Scheme 完全不能以任何方式区别两个对象的时候返回真。eqv? 检查两个对象在“操作等价”概念上的是否一致[Rees and Clinger (1986), 13]。在多数实际上有关的情况下结果与 eq? 相同。equal? 实现一个更弱等价概念,如果对象“打印同样的东西”则为等价。 (eqv? (list 'a) (list 'a)) ; 它们 "看起来一样",但是
;()
(eq? (list 'a) (list 'a)) ; 存储在不同的内存单元中。
;()
(equal? (list 'a) (list 'a))
;#t
Scheme 识别整数、实数和复数,可以按习惯的方式去写:
42
;42
3.14
;3.14
1.0e10
;10000000000
提供了多数的谓词(number?, zero?, positive?, negative?, odd?, even?),比较(=, <, <=, >, >=),算术(+, -, *, /, remainder, exp, expt, sqrt, log, floor, ceiling, round, truncate, ...),和三角(sin, cos, ...)函数,它们都带有明显的行为。 (number? 7)
;#t
(odd? 3.14)
;ERROR: Non-integer argument to function - 3.14
(/ 3 2)
;1.5
(expt 2 3)
;8
(floor 3.14)
;3
(round 7.7)
;8
(= 7 13)
;()
在 Scheme 的多数实现中空表等同于 #f,这解释了为什么上面的例子返回空表。我们还可以选择最大值和或最小值,提供了很多到其他表示形式的变换(integer->char, number->string, ...)。 (number->string (sqrt 3.14))
;"1.772"
这个例子还展示了表达式是可以被嵌入的,在这种情况以正常方式(“从内至外”并且“从左至右”)进行求值。优先级规则明显的是没有必要的,因为 Cambridge Polish 表示法强制我们总是提供完整的圆括号表达式。
#t 和 #f 是两个“一般的”逻辑值。出于方便任何值都可以在条件测试中用做逻辑值。在这个上下文中不是 #f 和空表的所有值都被认为是“真”。提供了一个谓词(boolean?)和一些逻辑操作(and, or, not) (boolean? '())
;#t
(and #t #t)
;#t
(or #f (< 7 0))
;()
字符是表示可打印的字符的对象,使用记号 # 作为前导。例如 #x
;120
#y
;121
#space
;32
这里的返回的数值是字符的 ASCII 编码等价。提供了谓词(char?)、一些比较(char=?, char>?, ...)和一些转换(char->integer, integer->char)过程。
字符串是包围在双引号(")内的字符序列。使用反斜杠()来在字符串中包含双引号,例如 (display "the " character delimits a string")
;the " character delimits a string
要在字符串中包含反斜杠必须使用另一个反斜杠来逃逸它。Scheme 提供各种创建(make-string)、谓词(string?, string-null?)、比较(string=?, ...)、选择(string-length, string-ref, substring)、变更(string-append, ...)和转换(string->list, list->string)操作符。 (string-length "mumble")
;6
(string-append "mumble " "mumble")
;"mumble mumble"
(string-ref "mumble" 4)
;108
上面的例子展示了字符串索引开始于 0,因为 "108" 是 "l" 的 Ascii 码表示。
不是迄今为止提到了的那些符号必被引用来强制为文字解释。 MeaningOfLife
;42
(quote MeaningOfLife)
;MeaningOfLife
(quote X)
;X
X
;ERROR: Undefined global variable X
在最后的情况下,解释器尝试获得符号的值,它以前没有被绑定。因为不存在这么个值,程序停止并且可能调用一个调试器。Scheme 使用 define 来介入变量并绑定它们到一个初始值,可以接着通过(使用 set!)副作用改变它。 (define Capnomancy "study of smoke to determine the future")
;Capnomancy
(set! MeaningOfLife Capnomancy)
;MeaningOfLife
MeaningOfLife
;"study of smoke to determine the future"
现在我们要注意 set! 可以用来改变一个符号的值,还要注意 Scheme 的“动态类型”本质将允许我们把一个数值绑定(比如MeaningOfLife 的"42")替代为不同数据类型的实例。可以使用谓词(symbol?)和变换过程(string>symbol)来操作符号。 (symbol? (string->symbol "Capnomancy"))
;#t
表是 Scheme 最重要的结构,因为它们用于“数据”和“程序”二者。明显的,它们必须被引用来充当 “Lisp-材料的惰性块”[Hofstadter (1983)]。 '(HappyMole LittleBear LittleTiger)
;(HappyMole LittleBear LittleTiger)
是有 3 个元素的一个(被引用的)表。去除这些引用是要惹祸的。 (HappyMole LittleBear LittleTiger)
;ERROR: undefined global variable LittleTiger
Scheme 将尝试把表处理为把叫做“HappyMole”的过程应用到余下的元素。结果的错误消息再次指出 MacScheme 杂乱无序的处理表元素,因此无法找到对“littleTiger”的任何绑定。即使对所有元素的绑定都存在,求值也可能失败。 MeaningOfLife
;"study of smoke to determine the future"
('MeaningOfLife)
;ERROR: Bad procedure MeaningOfLife
(MeaningOfLife)
;ERROR: Bad procedure "study of smoke to determine the future"
在第一种情况下我们要求符号的当前绑定,而在第二种情况下我们尝试“执行”这个符号,在第三种情况下我们尝试执行这个符号的值。回想起来,因为表是未被引用的,Scheme 将把它解释为一个“程序”。依据 Cambridge Polish 表示法的规则,它的第一个元素应当指示一个过程。在第二和第三种情况,它实际引用到一个名字或一个数值,它们当然不能被“执行”了。
Scheme 自诩提供操作表的丰富的自带的过程。它提供谓词(pair?, null?)、构造(cons, list, append)、 选择(length, car, cdr, ... list-ref, list-tail, assoc, member, memq) 和变换(list->string, list->vector, ...)。
表在内部存储为叫做“点对”的树结构中。这种表示可以回溯到第一个 Lisp 解释器,它建造在 IBM 704 上,它的 36-bit 内存单元可以分解成所谓的“减量”和“地址”两部分。所以一个内存单元可以用来存储要么一个表元素要么两个指针。接着从元素(“地址寄存器的内容” - car)和到子表的引用(“减量寄存器的内容” - cdr)(递归的)构造表。cons 充当表构造器。它接受一个元素和一个表作为参数并返回一个新构造的表作为结果。请注意空表(),在表处理操作中充当 0 在整数算术中的那种作用。你可以通过加到零来枚举所有的数,你也可以同样的通过增加到空表来建造表。这个过程经常被称为“构造表”。即使多数实现对待 #f 与空表是一致的,在提及空表的时候使用 () 而不是 #f 是好习惯,保留 #f 用做“假”值。如果传递给 cons 两个原子,它将制作一个“点对”;就是说,“地址”指针将设置为指向第一个参数,而“减量”指针指向第二个参数。在实际编程中直接操作点对是非常少见的,通常都提供更加方便的操作符用作表的构造。list 将绑定任意多个参数到一个表结构中,可以使用 append 联接 2 个表。 (cons 'LittleBear 'LittleTiger) ; 制作一个“点对”
;(LittleBear . LittleTiger)
(cons 'HappyMole (cons 'LittleBear (cons 'LittleTiger #f)))
; 同图 2.12 中一样
;(HappyMole LittleBear LittleTiger)
; 或者使用更容易的方式:
(list 'HappyMole 'LittleBear 'LittleTiger)
;(HappyMole LittleBear LittleTiger)
(append '(HappyMole LittleBear LittleTiger) '(AuntyGoose))
;(HappyMole LittleBear LittleTiger AuntyGoose)
pair? 检查它的参数是否是一个“点对”,还可以用于测试“表身份”(listhood)。 (pair? '(LittleTiger))
;#t
null? 检查它的参数是否是空表。 (null? ())
;#t
对 #f 做同 () 一样处置的实现也返回 #t。 (null? #f)
;#t
提供各种过程支持选择表的特定元素。car 和 cdr 是其中最重要的。使用这样的名字只有历史性的理由,现在已经完全不适用了,但是 Lisp 社区坚定的抵制朝向更有意义的任何改变(比如,head 和 tail,或 first 和 last)。当然,car 代表“地址寄存器的内容”而引用在表头部的元素。可以使用 cdr(“减量寄存器的内容”)来取回所有余下元素的子表。请注意 car 总是返回一个单一元素,而在应用到一个表的时候,总是返回一个表(可能为空)! 尝试对一个空表的 cars 或 cdrs 将导致一个错误情况,与尝试除以零是一样的。
使用car 和 cdr 的组合可以逐个处理表的每个元素。在应用到(被引用的!)表(troll hydra banshee gryphon vampire)的时候,cdr 将产生表(hydra banshee gryphon vampire),而 car 将返回 troll。要得到第 5 个元素,我们必须调用 cdr 四次。这将返回表(vampire),这时 car 将接者产生想要的元素。Scheme 允许这些访问函数的联接,直到第 4 层。(car (cdr (cdr ( cdr (cdr '(troll hydra banshee gryphon vampire)))))) 可写成 (cadddr (cdr '(troll hydra banshee gryphon vampire)))。容易造成“口吃”可能是改变 car 和 cdr 名字的一个勉强的理由。实际上有更方便的方式来访问表的第 5 个元素。list-ref 可用于此目的。 (length '(troll banshee vampire))
;3
(car '(troll banshee vampire))
;troll
(cdr '(troll banshee vampire))
;(banshee vampire)
(car (cdr (cdr '(troll banshee vampire))))
;vampire
(caddr '(troll banshee vampire))
;vampire
(list-tail '(troll banshee vampire) 2)
;(vampire)
(list-ref '(troll banshee vampire) 2)
;vampire
只有最后两个例子值得说明。list-tail 通过除去前面的 n 个元素来获得一个子表,而 list-ref 返回在指定位置的元素。要注意 list-ref 从零开始计数!
还有用于查找表的一些过程。member 和 memq 将返回它的 car 是一个指定元素的一个子表。它们使用不同的过程(equal? 和 eq?)来测试等同性。请注意它们返回表的事实不防碍它们用在(逻辑值)条件中。这是把所有非 nil 值作为“真”对待的惯例派上用场的一种情况。“关联表”是一类特殊的表,它存储“键-值”绑定。它们可以被表示为有两个元素的表的表,而且 Scheme 提供一个 assoc 过程来检索一个值,它需要一个键作为参数。
|