兼容性(Compatibility Features in Chez Scheme)

本章介绍了当前版本的Chez Scheme中包含的几个项目,主要是为了与系统的较早版本兼容。由于兼容功能可能在未来会被放弃,所以新项目中应尽可能使用Scheme标准机制。

Hash Tables

略过。应使用标准hash table.

Extend-Syntax Macros

本节介绍了extend-syntax,它是一种功能强大但易于使用的基于模式匹配的语法扩展工具。 使用extend-syntax编写的语法转换与使用define-syntaxsyntax-case编写的语法转换相似,不同之处在于extend-syntax产生的转换不会自动遵循词法作用域。

通常不可能将使用syntax-case编写的语法抽象与使用extend-syntax编写的语法抽象无缝地混合在一起。 通常尽可能只使用其中一种。 仅在迁移到syntax-case时提供了对syntax-case扩展器中extend-syntax的支持。

(extend-syntax (name key …) (pat fender template) …)

标识符name 是要定义的句法扩展名或语法关键字。 当系统扩展器处理car为name 的任何list表达式时,将在该表达式上调用extend-syntax生成的语法转换过程。 其余的标识符key … 是在扩展过程中要在输入表达式中识别的其他关键字(例如cond中的else或case)。

key列表之后的每个子句都包含一个模式pat ,一个可选的fender 和一个template 。 可选的fender 经常被省略。pat 指定了语法,用于匹配子句。模式中不是关键字(模式变量)的标识符绑定到输入表达式的相应部分。fender 如果存在,则是Scheme表达式,它指定输入表达式(通过模式变量访问)上的附加约束,必须选择这些约束才能选择子句。template 通常根据模式变量来指定输出采用什么形式。

在扩展过程中,转换过程extend-syntax会尝试以给定的子句的顺序将输入表达式与每个模式进行匹配。如果输入表达式匹配了某个模式,则将模式变量绑定到输入表达式的相应部分,并对子句的fender (如果有)进行求值。如果fender 返回一个真值,则执行给定的扩展。 如果输入与模式不匹配,或者fender 返回错误值,则转换过程将尝试下一个子句。如果无法选择任何子句,则会引发条件类型**&assertion**的异常。

在模式内,省略号(…)可用于指定零个或多个出现的前面的模式片段或原型。类似地,可以在输出中使用省略号来指定零个或多个扩展原型的结构。在这种情况下,扩展原型必须包含输入模式原型的一部分。

第一个例子,定义了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的定义显示了多个省略号的使用,其中一个用于标识符/值序对,另一个用于body中的表达式。它还表明原型不必是单个标识符,并且在template中可以将原型的各个部分彼此分开。

(extend-syntax (let)
  [(let ([x e] ...) b1 b2 ...)
   ((lambda (x ...) b1 b2 ...) e ...)])

下一个示例显示let*,其语法与let相同,但是根据let以两个子句(一个用于基本情况,一个用于递归步骤)递归定义,因为它必须产生一个嵌套结构。

(extend-syntax (let*)
  [(let* () b1 b2 ...)
   (let () b1 b2 ...)]
  [(let* ([x e] more ...) b1 b2 ...)
   (let ([x e]) (let* (more ...) b1 b2 ...))])

定义式and需要3个子句。第一个子句对于识别**(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 ...))])

为了使let的语法绝对正确,我们实际上必须要求输入中的绑定标识符是符号。 如果我们输入类似**(let([3 x])x)的内容,则不会从let**中得到错误,因为它不会检查验证标识符位置中的对象是否为符号。 相反,lambda可能会抱怨,或者可能是扩展完成很久之后的求值程序。 这是fenders 起作用的地方。

(extend-syntax (let)
  [(let ([x e] ...) b1 b2 ...)
   (andmap symbol? '(x ...))
   ((lambda (x ...) b1 b2 ...) e ...)])

’(x …)上的symbol?andmap确保每个绑定标识符都是一个符号。 fender 仅仅是Scheme表达式。 在该表达式中,首先使用与子句的template 部分相同的规则来扩展引用的对象。 在这种情况下,将**’(x …)**扩展到标识符/值对中的标识符列表。

extend-syntax通常可以处理您的一切需求,但是某些语法扩展定义要求能够包含对任意Scheme表达式求值的结果。 该功能由with提供。

(with ((pat expr) …) template)

with仅在extend-syntax内部的template 内有效。 with模式与extend-syntax模式相同,with表达式与extend-syntaxfenders 相同,with模板与extend-syntax模板相同。

with可用于引入新的模式标识符,该标识符绑定到extend-syntax模板中的任意Scheme表达式所生成的表达式。 也就是说,with允许从extend-syntax的声明式样式转为full Scheme的过程式样式。

with的一种常见用法是在模板中引入临时标识符或临时标识符列表。 如果在extend-syntax框架内执行,with也可用于执行可能笨拙或效率低下的复杂转换。

例如,or需要使用临时标识符。 我们可以如下定义or

(extend-syntax (or)
  [(or) #f]
  [(or x) x]
  [(or x y ...)
   (let ([temp x])
     (if temp temp (or y ...)))])

这会一直有效,直到将or表达式放置在temp发生的范围内为止,在这种情况下,可能会发生奇怪的事情,因为extend-syntax不尊重词法作用域。 (这是define-syntax优于extend-syntax的原因之一。)

(let ([temp #t])
  (or #f temp))  #f

如果将标识符temp换个名字,则一切正常。就可以理解"extend-syntax不尊重词法作用域"这句话了。 实际上,上述的(let …)表达式,展开之后就变成了:

(let ([temp #t])
   (let ([temp #f])
       (if temp temp (or temp))
   )
)

一种解决方案是,使用gensymwith来创建临时标识符,如下:

(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示例:

(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表达式的其余部分区分开。

下面的示例利用with允许我们将模式绑定到表达式这一事实,将模式变量列表绑定到临时符号列表。 此临时列表帮助我们实现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)

结构体

本节介绍一种机制,类似于第7.15节的record定义机制,该机制允许使用固定的命名字段集创建数据结构。 与record类型不同,结构体类型不是唯一类型,而是实现为向量。 具体而言,将结构体实现为向量,其长度比字段数大一倍,并且其第一个元素包含该结构体的符号名称。

将结构体表示为向量可以在某种程度上简化结构体的读取和打印以及结构体定义工具的扩展。但是,它确实有一些缺点。 一个是在不适当的情况下,结构体可能会被错误地视为普通向量。当在程序中处理结构体和向量时,在检查更通用的向量类型之前,必须注意先寻找更具体的结构体类型,例如在一系列cond子句中。一个类似的缺点是,结构体实例容易被有意或无意地“伪造”。 也不可能控制如何打印和读取结构体。

通过define-structure创建结构体。 每个结构体定义式都定义一个构造过程,一个类型谓词,每个字段的访问过程以及每个字段的分配过程。define-structure允许程序员控制哪些字段是构造函数过程的参数,以及哪些字段由构造函数过程显式初始化。define-structure非常简单,但对于大多数应用程序来说足够强大,如果还不足以应付应用程序的需求还可以很容易的扩展以满足之。 本节末尾给出的define-structure定义可以作为更复杂变体的起点。

(define-structure (name id1 …) ((id2 expr) …))

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中定义pair(如果它们不存在的话)。 这两个字段都由构造函数过程的参数初始化。

(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

以下示例定义了一个方便的字符串数据结构,称为strext-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)  "@@@@@@@="

《The Scheme Programming Language》(第4版)的8.4节定义了define-structure的简化变体,作为使用syntax-case的示例。 下面给出的定义实现了完整版本。

(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)))
             ...))))))