第16章 兼容功能 (Compatibility Features)
本章介绍了当前版本的 Chez Scheme中包含的几个项目,主要是为了与较早版本的系统兼容。
16.1节 描述了一个哈希表接口,此接口从现在起被 R6RS哈希表接口所取代。16.2节 描述了扩展语法宏。当前版本的 Chez Scheme直接支持这些功能,但在将来的版本中可能不再支持。新程序应改用 The Scheme Programming Language,第4版 [11]中描述的标准机制。
16.3节 描述了一种将类似记录的结构定义为向量而不是新的唯一类型的机制。新程序应改用 define-record
(在7.15节)中介绍)。
16.4节 描述了与 Chez Scheme一起分发的兼容性文件,其中包含 Chez Scheme不再直接支持的表单和过程的定义。
16.1节 哈希表 (Hash Tables)
这里的哈希表过程被第7.12节中列出的新哈希表过程所取代 (原文是 "be obviated")。过程:(make-hash-table)
过程:(make-hash-table weak?)
返回:一个新的哈希表
所在库:(chezscheme)
如果使用了 weak?
参数且该参数不是 #f
,那么哈希表是弱哈希表,这意味着它不保护密钥不受垃圾收集器的影响。垃圾回收器回收的键将从表中删除,它们的关联值将在下次修改表时(如果不是更早的话)被删除。
过程:(hash-table? obj)
返回:如果 obj
是哈希表,则返回 #t
,否则返回 #f
所在库:(chezscheme)
过程:(put-hash-table! ht k v)
返回:未指定的
所在库:(chezscheme)
ht必须是哈希表。k和 v可以是任何 Scheme值。
put-hash-table!
会在 ht中关联值 v与键 k
过程:(get-hash-table ht k d)
返回:参见以下
所在库:(chezscheme)
get-hash-table
返回 ht中与 k关联的值。如果 ht中没有与 k相关联的值,则 get-hash-table
返回默认值 d。
键之间通过 eq?
进行比较。
因为对象可能被垃圾收集器移动,所以 get-hash-table
可能需要重新哈希一些对象,因此在哈希表中引起副作用。因此,使用 get-hash-table
从多个线程对同一哈希表进行并发访问是不安全的。
过程:(remove-hash-table! ht k)
返回:未指定的
所在库:(chezscheme)
remove-hash-table!
从 ht中删除任何与 k的关联。
过程:(hash-table-map ht p)
返回:参见以下
所在库:(chezscheme)
hash-table-map
将 p应用于每个键,以 ht的值关联(不按特定顺序),并再次以不特定的顺序返回结果值的列表。p接受两个参数,一个键和一个值。
过程:(hash-table-for-each ht p)
返回:未指定的
所在库:(chezscheme)
每个哈希表将 p应用于每个键,以 ht表示的值关联,没有特定的顺序。与 hash-table-map
不同,它不创建值列表;相反,它的值是不确定的。p接受两个参数,一个键和一个值。
16.2节 扩展语法宏 (Extend-Syntax Macros)
本节介绍了extend-syntax
,这是一种功能强大且易于使用的基于 模式匹配[27]。使用 extend-syntax
编写的语法转换与使用 define-syntax
和 syntax-case
编写的语法转换 相似,不同之处在于,extend-syntax
产生的转换不会自动遵循词法作用域。
通常不可能无缝地将使用语法大小写编写的语法抽象与使用扩展语法编写的语法抽象混合在一起。通常优选在任何可能的地方使用一个或另一个。在语法用例扩展器中对扩展语法的支持仅是作为迁移到语法用例的辅助。
语法:(extend-syntax (name key ...) (pat fender template) ...)
返回:未指定的
所在库:(chezscheme)
标识符名称是要定义的语法扩展名的名称或语法关键字。当系统扩展器处理 car
为 name的任何列表表达式时,将对此表达式调用由 extend-syntax
生成的语法转换过程。剩余的标识符 key ...
是在展开期间要在输入表达式中识别的附加关键字(例如 cond
或case
中的 else
)。
key列表之后的每个子句都包含一个模式 pat
,一个可选的挡板 fender
和模板 template
。可选的 fender
经常被省略。pat
指定要选择的子句的输入表达式必须具有的语法。模式中不是关键字(即模式 pat
中的变量)的标识符绑定到输入表达式的相应部分。如果存在的话,fender
是一个 Scheme表达式,它指定输入表达式(通过模式变量访问)上必须满足的附加约束,以便选择子句。模板指定输出的形式,通常是根据模式变量。
在扩展过程中,转换过程 extend-syntax
会生成尝试以子句给定的顺序将输入表达式与每个模式进行匹配。如果输入表达式与模式匹配,则将模式变量绑定到输入表达式的相应部分,并对子句的 fender
(如果有的话)进行求值。如果 fender
返回一个真值,则执行给定的扩展。如果输入与模式不匹配或 fender
返回错误值,则转换过程将尝试下一个子句。如果无法选择任何子句,则会引发条件类型 &assertion
的异常。
在模式中,省略号(...)可用于指定前一个模式片段或原型的零次或多次出现。类似地,可以在输出中使用省略号来指定零个或多个扩展原型的构造。在这种情况下,扩展原型必须包含输入模式原型的一部分。模式、模板、模板内的省略号和挡板的使用将在下面的一系列示例中进行说明。
第一个例子,定义 rec
,用了一个关键字 rec
,一个没有 fender
的子句,并且没使用省略号。
(extend-syntax (rec)
[(rec id val)
(let ([id #f])
(set! id val)
id)] )
第二个例子,定义 when
,展示了省略号的用法。
(extend-syntax (when)
[(when test exp1 exp2 ...)
(if test (begin exp1 exp2 ...) #f)])
下一个示例显示了 let
。let
的定义显示了多个省略号的使用,其中一个用于标识符/值的序对,另一个用于主体中的表达式。它还表明原型不必是单个标识符,并且原型的各个部分可以在模板中彼此分开。
(extend-syntax (let)
[(let ([x e] ...) b1 b2 ...)
((lambda (x ...) b1 b2 ...) e ...)])
下一个示例显示 let*
,其语法与 let
相同,但它是用两个子句(一个用于基本情况,一个用于递归步骤)递归定义的,因为它必须生成嵌套结构。
(extend-syntax (let*)
[(let* () b1 b2 ...)
(let () b1 b2 ...)]
[(let* ([x e] more ...) b1 b2 ...)
(let ([x e]) (let* (more ...) b1 b2 ...))])
第一个模式/模板序对会匹配没有标识符/值序对的任何 let*
表达式,并将其映射到等效的 begin
表达式中。这是基本情况。第二个模式/模板序对将任何 let*
表达式与一个或多个标识符/值序对匹配,并将其转换为绑定第一个序对的 let
表达式,主体的主体是 let*
表达式,其绑定其余序对。这是递归步骤,最终将我们引向基本情况,因为我们在每一步都删除了一个标识符/值序对。请注意,第二个模式更多地使用了模式变量用于第二和以后的标识符/值序对;这样可以减少模式和模板的混乱程度,并且可以清楚地看到只有第一个标识符/值序对被明确处理。
and
的定义需要三个子句。第一个子句是识别(and)
所必需的,后两个子句递归地定义其他 and
的形式。
(extend-syntax (and)
[(and) #t]
[(and x) x]
[(and x y ...) (if x (and y ...) #f)])
cond
的定义需要四个子句。与 let*
一样,必须对 cond
进行递归描述,部分原因是它会产生嵌套的 if
表达式,部分原因是一个省略号原型不足以描述所有可能的 cond
子句。cond
的定义还要求除了 cond
之外,还要将 else
指定为关键字。这是定义:
(extend-syntax (cond else)
[(cond) #f]
[(cond (else e1 e2 ...))
(begin e1 e2 ...)]
[(cond (test) more ...)
(or test (cond more ...))]
[(cond (test e1 e2 ...) more ...)
(if test
(begin e1 e2 ...)
(cond more ...))])
其中两个子句是基本情况,两个子句是递归步骤。第一个基本情况是空的条件。在这种情况下,cond
的值是不确定的,因此 #f的选择 有些随意。第二种基本情况是只包含 else子句的 cond
;它将转换为等效的 begin表达式。两个递归步骤在 cond
子句中的表达式数量不同。当第一个 true子句仅包含测试表达式时,cond
的值就是测试的值。这类似于或做什么,因此我们将 cond
子句扩展为 or
或表达。另一方面,当测试表达式之后有表达式时,将返回最后一个表达式的值,因此我们使用 if和 begin。
为了使 let
的语法绝对正确,我们实际上必须要求输入中的绑定标识符 x ...
是符号。如果我们输入类似 (let ([3 x]) x)
的内容,而没有在 let
中得到错误,是因为它没去检查验证标识符位置中的对象是否为符号; 那么,lambda
可能会抱怨,或者可能是扩展完成很久之后系统的求值器要抱怨。所以这就是挡板的好处。
(extend-syntax (let)
[ (let ([x e] ...) b1 b2 ...)
(andmap symbol? '(x ...))
((lambda (x ...) b1 b2 ...) e ...) ] )
andmap
相当于 (compose and map)
, 使用 andmap
检查 '(x ...)
, 确保每个绑定标识符都是一个符号。fender
只是 Scheme表达式。在该表达式中,首先使用与子句的模板部分相同的规则来扩展任何引用的对象。在这种情况下,'(x ...)
被扩展到标识符/值对中的标识符列表。
扩展语法通常可以处理您需要的所有内容,但是某些语法扩展定义要求能够包含评估任意 Scheme表达式的结果。此功能由提供。
语法:(with ((pat expr)...) template)
返回:已处理的模板
with
仅在扩展语法内的模板内有效。with
模式与 extend-syntax
模式相同,with
表达式与 extend-syntax
fender相同,with
模板与 extend-syntax
模板相同。
可以使用 with
将新的模式标识符绑定到扩展语法模板中由任意方案表达式生成的表达式。也就是说,with
可以从扩展语法的声明式样式转为完整的 Scheme过程样式。
with
的一个常见用法是在模板中引入临时标识符或临时标识符列表。with
还用于执行复杂的转换,如果在扩展语法框架中执行,转换可能会很笨拙或效率低下。
例如,或要求使用临时标识符。我们可以定义或如下。
(extend-syntax (or)
[(or) #f]
[(or x) x]
[(or x y ...)
(let ([temp x])
(if temp temp (or y ...)))])
这种方法工作正常,直到我们将或表达式放置在 temp发生的范围之前,在这种情况下,可能会发生奇怪的事情,因为 extend-syntax
不遵循词法检查。(这是 define-syntax
优于 extend-syntax
的原因之一。)
(let ([temp #t])
(or #f temp)) => #f
一种解决方案是使用 gensym
和 with
创建临时标识符,如下所示。
(extend-syntax (or)
[(or) #f]
[(or x) x]
[(or x y ...)
(with ([temp (gensym)])
(let ([temp x])
(if temp temp (or y ...))))])
同样,with
可以用来以扩展语法无法直接实现的方式组合输入模式的元素,例如以下折叠加示例。
(extend-syntax (folding-plus)
[(folding-plus x y)
(and (number? 'x) (number? 'y))
(with ([val (+ 'x 'y)])
val)]
[(folding-plus x y) (+ x y)])
如果 x和 y均为数字常量,则 folding-plus
折叠为(+ x y)
的值。否则,folding-plus
将转换为(+ x y)
以供以后评估。fender在扩展时检查操作数是否为数字,with
执行求值。与 fender一样,扩展仅在带引号的表达式内执行,因为 quote
将数据与 Scheme表达式的其余部分区分开。
下面的示例利用了允许我们将模式绑定到表达式的事实,将模式变量列表绑定到临时符号列表。此临时列表帮助我们实现 sigma
句法扩展。sigma
与 lambda
相似,除了它在标识符列表中分配标识符而不是创建新的绑定。它可以用来并行执行一系列分配。
(extend-syntax (sigma)
[(sigma (x ...) e1 e2 ...)
(with ([(t ...) (map (lambda (x) (gensym)) '(x ...))])
(lambda (t ...)
(set! x t) ...
e1 e2 ...))])
(let ([x 'a] [y 'b])
((sigma (x y) (list x y)) y x)) => (b a)
下面的最后一个示例使用扩展语法来实现 define-structure
,这与 Scheme编程语言第4版第8.4节中使用语法 case
的类似示例相同。
define-structure
的定义使用了两个 pattern / template子句。需要两个子句来处理第二个子表达式的可选性。第一个子句与没有第二个子表达式的形式匹配,仅将其转换为存在第二个子表达式的等效形式,但为空。
该定义还大量使用 with
在展开时计算方案表达式。前四个 with
子句用于生成标识符,这些标识符命名自动定义的过程。(过程格式在这里特别有用,但是可以用 string-append!
替换,根据需要使用 symbol->string
。)前两个子句生成单个标识符(用于构造函数和谓词),而后两个子句生成标识符列表(用于字段访问和赋值过程)。第五个 with
子句(外部 with中的最后一个子句)用于计算结构的每个实例所需的总长度向量,其中必须包括名称和所有字段的空间。最后的 with
子句(内部 with
中的唯一子句)用于创建向量索引列表,每个字段一个索引(从1开始,因为结构名称占据位置0)。
(extend-syntax (define-structure)
[(define-structure (name id1 ...))
(define-structure (name id1 ...) ())]
[(define-structure (name id1 ...) ([id2 val] ...))
(with ([constructor
(string->symbol (format "make-~a" 'name))]
[predicate
(string->symbol (format "~a?" 'name))]
[(access ...)
(map (lambda (x)
(string->symbol
(format "~a-~a" 'name x)))
'(id1 ... id2 ...))]
[(assign ...)
(map (lambda (x)
(string->symbol
(format "set-~a-~a!" 'name x)))
'(id1 ... id2 ...))]
[count (length '(name id1 ... id2 ...))])
(with ([(index ...)
(let f ([i 1])
(if (= i 'count)
'()
(cons i (f (+ i 1)))))])
(begin
(define constructor
(lambda (id1 ...)
(let* ([id2 val] ...)
(vector 'name id1 ... id2 ...))))
(define predicate
(lambda (obj)
(and (vector? obj)
(= (vector-length obj) count)
(eq? (vector-ref obj 0) 'name))))
(define access
(lambda (obj)
(vector-ref obj index)))
...
(define assign
(lambda (obj newval)
(vector-set! obj index newval)))
)))))))))))
16.3节 结构体 (Structures)
本节介绍一种机制,类似于7.15节的record-defining
机制,该机制 允许使用固定的命名字段集创建数据结构。与记录类型不同,结构类型不是唯一类型,而是实现为向量。具体而言,将结构实现为向量,其长度比字段数大一倍,并且其第一个元素包含该结构的符号名称。
将结构表示为矢量可以在某种程度上简化结构的读取和打印以及结构定义工具的扩展。但是,它确实有一些缺点。一个是在不适当的情况下,结构可能会被错误地视为普通向量。当在程序中处理结构和向量时,在检查更通用的向量类型之前,例如在一系列 cond
子句中,必须注意寻找更具体的结构类型。一个类似的缺点是,结构实例容易被有意或无意地“伪造”。也不可能控制如何打印和读取结构。
通过 define-structure
创建结构。每个结构定义都定义一个构造函数过程,一个类型谓词,每个字段的访问过程以及每个字段的赋值过程。 define-structure
允许程序员控制哪些字段是所生成的构造函数过程的参数,以及哪些字段由构造函数过程显式初始化。
define-structure
很简单,但对于大多数应用程序来说足够强大,并且可以很容易地扩展以处理许多应用程序,但这些结构还不够。本节末尾给出的定义结构定义可以作为更复杂变体的起点。
语法:(define-structure (name id1 ...) ((id2 expr) ...))
返回:未指定的
所在库:(chezscheme)
define-structure
形式是一种定义,可以出现在任何地方,并且只能出现在其他定义出现的地方。
define-structure
定义一个新的数据结构 name,并创建一组用于创建和操作该结构实例的过程。标识符 id1 ...
和 id2 ...
命名数据结构的字段。
以下过程由 define-structure
定义:
- 一个名为
make-name
的构造函数过程, - 一个名为
name?
的类型谓词, - 一个访问过程,其名称是每个字段名
id1 ...
和id2 ...
的name-field
,以及 - 一个用于每个字段名
id1 ...
和id2 ...
, 名为set-name-field!
的赋值过程。
由标识符 id1 ...
命名的字段是由构造函数过程的参数初始化的。由标识符 id2 ...
命名的字段被显式初始化为表达式 expr ...
的值。每个表达式在标识符 id1 ...
的范围内求值(绑定到相应的字段值)和任何标识符 id2 ...
(绑定到相应的字段值)出现在它前面(就像在 let*
中一样)。
为了说明这一点,构造函数的行为可以定义为
(define make-name
(lambda (id1 ...)
(let* ([id2 expr] ...)
body ) ) )
其中 body根据标识符 id1 ...
和 id2 ...
的值构建结构。
如果不需要除构造函数过程的参数初始化的字段以外的其他字段,则可以省略第二个子表达式 ((id2 expr) ...)
。
下面的简单示例演示了如果不存在对,如何在 Scheme中定义它们。这两个字段都由构造函数过程的参数初始化。
(define-structure (pare car cdr))
(define p (make-pare 'a 'b))
(pare? p) => #t
(pair? p) => #f
(pare? '(a . b)) => #f
(pare-car p) => a
(pare-cdr p) => b
(set-pare-cdr! p (make-pare 'b 'c))
(pare-car (pare-cdr p)) => b
(pare-cdr (pare-cdr p)) => c
下面的示例定义了一个方便的字符串数据结构,称为stretch-string
,可以根据需要进行增长。此示例使用显式初始化为依赖于构造函数参数字段值的值的字段。
(define-structure (stretch-string length fill)
([string (make-string length fill)]))
(define stretch-string-ref
(lambda (s i)
(let ([n (stretch-string-length s)])
(when (>= i n) (stretch-stretch-string! s (+ i 1) n))
(string-ref (stretch-string-string s) i))))
(define stretch-string-set!
(lambda (s i c)
(let ([n (stretch-string-length s)])
(when (>= i n) (stretch-stretch-string! s (+ i 1) n))
(string-set! (stretch-string-string s) i c))))
(define stretch-string-fill!
(lambda (s c)
(string-fill! (stretch-string-string s) c)
(set-stretch-string-fill! s c)))
(define stretch-stretch-string!
(lambda (s i n)
(set-stretch-string-length! s i)
(let ([str (stretch-string-string s)]
[fill (stretch-string-fill s)])
(let ([xtra (make-string (- i n) fill)])
(set-stretch-string-string! s
(string-append str xtra))))))
通常,大多数自动定义的过程仅用于定义更专门的过程,在这种情况下,这些过程为 stretch-string-ref
和 stretch-string-set!
。stretch-string-length
和 stretch-string-string
是唯一可能在使用拉伸字符串的代码中直接调用的自动定义的过程。
(define ss (make-stretch-string 2 #\X))
(stretch-string-string ss) => "XX"
(stretch-string-ref ss 3) => #\X
(stretch-string-length ss) => 4
(stretch-string-string ss) => "XXXX"
(stretch-string-fill! ss #\@)
(stretch-string-string ss) => "@@@@"
(stretch-string-ref ss 5) => #\@
(stretch-string-string ss) => "@@@@@@"
(stretch-string-set! ss 7 #\=)
(stretch-string-length ss) => 8
(stretch-string-string ss) => "@@@@@@@="
Scheme编程语言第4版第8.4节定义了 define-structure的简化变体,作为使用语法 case的示例。下面给出的定义实现了完整的版本。
define-structure
扩展为一系列由结构名称和字段名称生成的名称的定义。生成的标识符使用 datum->syntax
使标识符在define-structure
形式出现的地方可见。由于 define-structure
表单扩展为包含定义的 begin,因此它本身就是一个定义,可以在定义有效的地方使用。
(define-syntax define-structure
(lambda (x)
(define gen-id
(lambda (template-id . args)
(datum->syntax template-id
(string->symbol
(apply string-append
(map (lambda (x)
(if (string? x)
x
(symbol->string
(syntax->datum x))))
args))))))
(syntax-case x ()
((_ (name field1 ...))
(andmap identifier? #'(name field1 ...))
#'(define-structure (name field1 ...) ()))
((_ (name field1 ...) ((field2 init) ...))
(andmap identifier? #'(name field1 ... field2 ...))
(with-syntax
((constructor (gen-id #'name "make-" #'name))
(predicate (gen-id #'name #'name "?"))
((access ...)
(map (lambda (x) (gen-id x #'name "-" x))
#'(field1 ... field2 ...)))
((assign ...)
(map (lambda (x) (gen-id x "set-" #'name "-" x "!"))
#'(field1 ... field2 ...)))
(structure-length
(+ (length #'(field1 ... field2 ...)) 1))
((index ...)
(let f ([i 1] [ids #'(field1 ... field2 ...)])
(if (null? ids)
'()
(cons i (f (+ i 1) (cdr ids)))))))
#'(begin
(define constructor
(lambda (field1 ...)
(let* ([field2 init] ...)
(vector 'name field1 ... field2 ...))))
(define predicate
(lambda (x)
(and (vector? x)
(#3%fx= (vector-length x) structure-length)
(eq? (vector-ref x 0) 'name))))
(define access (lambda (x) (vector-ref x index)))
...
(define assign
(lambda (x update) (vector-set! x index update)))
...))))))
16.4节 兼容性文件 (Compatibility File)
Chez Scheme的当前版本与兼容性文件一起发布,该兼容性文件包含 Chez Scheme早期版本支持的各种语法形式和过程的定义,此后不再提供支持。此文件 compat.ss通常安装在 Chez Scheme安装目录的 library子目录中。在某些情况下,由于不经常使用 compat.ss中的表格和过程,因此很容易直接将它们写在 Scheme中。 在其他情况下,系统的改进使表格和程序过时了。 在这种情况下,应编写新代码以使用较新的功能,并且应尽可能重写较旧的代码以使用较新的功能。
Chez Scheme 第 9 版用户手册
思科系统有限公司版权所有 © 2019
按 Apache License Version 2.0 授权(完整的版权公告)
这是 2019年 3月的修订版本, 对应于 Chez Scheme 9.5.2
关于本书