define-record-type
Scheme语言中的define-record-type形式用来定义一个记录类型,并定义该类型的构造函数、仅对该类型的record返回true的谓词、以及每个字段的访问procedure和针对可变字段的赋值procedure。总的来说,与Java中的POJO类比较相似,不过不用自定义getter和setter方法,这与Kotlin倒为类似,例如:
(define-record-type point (fields x y))
创建了一个名为point的record类型,并且有两个字段x和y,和如下几个定义式(自动生成):
(make-point x y) | 构造函数 |
(point? obj) | 谓词 |
(point-x p) | 字段x的访问器 |
(point-y p) | 字段y的访问器 |
默认下,字段是不可变的,但是可以声明为可变的(mutable)。如下定义point中,字段x是可变的,但y维持不变:
(define-record-type point (fields (mutable x) y))
当然亦可显式地将字段声明为不可变的:
(define-record-type point (fields (mutable x) (immutable y)))
在这个例子中,define-record-type除了生成了上述的几个定义式之外,还为字段x增加了一个赋值过程:
(point-x-set! p x)
该赋值过程可以用于改变x的内容
(define p (make-point 36 -17))
(point-x-set! p (- (point-x p) 12))
(point-x p) => 24
自动生成的几个定义式是允许改变名称的,下面的point定义式,其构造函数名为mkpoint, 谓词为ispoint?,x和y的访问器分别为x-val和y-val, x的赋值器为set-x-val!
(define-record-type (point mkpoint ispoint?)
(fields (mutable x x-val set-x-val!)
(immutable y y-val)))
默认情况下,每次一个record definition创建一个新类型(为方便理解,可以用Java来解释,即对于同一个POJO类new出两个实例,但这两个实例并不能equal),如下所示:
(define (f p)
(define-record-type point (fields x y))
(if (eq? p 'make) (make-point 3 4) (point? p)))
(f (f 'make)) => #f
第一个f的调用即(f ‘make)返回一个point类型的p,将p传递给第二个f,但p是由第一个调用生成的类型,所以point?返回#f。按照SICP的说法,这两个define-record-type并不在一个环境中,只是名字一样而已。
默认的生产行为(generative behavior)或许可以由记录定义式中的nongenerative子句来重载:
(define (f p)
(define-record-type point (fields x y) (nongenerative))
(if (eq? p 'make) (make-point 3 4) (point? p)))
(f (f 'make)) => #t
以这种方式创建的记录类型仍然不同于由定义出现在程序的不同部分中创建的记录类型,即使这些定义在语法上是相同的:
(define (f)
(define-record-type point (fields x y) (nongenerative))
(make-point 3 4))
(define (g p)
(define-record-type point (fields x y) (nongenerative))
(point? p))
(g (f)) => #f
甚至可以通过在nongenerative子句中包含uid(唯一id)来覆盖它:
(define (f)
(define-record-type point (fields x y)
(nongenerative really-the-same-point))
(make-point 3 4))
(define (g p)
(define-record-type point (fields x y)
(nongenerative really-the-same-point))
(point? p))
(g (f)) => #t
记录类型可以定义为有parent子句的子类型,即一个记录类型可以声明为某一个记录类型的子类型,如果指定了父类型,则子类型将继承父类型所有字段,且子类型的每个实例都被视为父类型的实例,因此可以直接使用父类型的访问器和字段等等。
(define-record-type point (fields x y))
(define-record-type cpoint (parent point) (fields color))
(define cp (make-cpoint 3 4 'red))
(point? (make-cpoint 3 4 'red)) => #t
(cpoint? (make-point 3 4)) => #f
(define cp (make-cpoint 3 4 'red))
(point-x cp) => 3
(point-y cp) => 4
(cpoint-color cp) => red
到目前为止,define-record-type定义的默认构造函数接受record包含的字段一样多的参数,其实我们可以重写默认值,这里需要引入protocol子句,以下定义将创建一个具有三个字段的点记录:x,y和d,其中d表示距原点的位移。构造函数仍然只接受两个参数,即x和y值,并将d初始化为x和y平方和的平方根。
(define-record-type point
(fields x y d)
(protocol
(lambda (new)
(lambda (x y)
(new x y (sqrt (+ (* x x) (* y y))))))))
(define p (make-point 3 4))
(point-x p) => 3
(point-y p) => 4
(point-d p) => 5
另外,子类型的构造函数中的参数顺序是不可改变的,即先是父类型构造函数的字段,然后才是子类型的参数,如果需要改变子类型的构造函数的参数顺序该如何呢?
(define-record-type cpoint
(parent point)
(fields color)
(protocol
(lambda (pargs->new)
(lambda (x c y)
((pargs->new x y) c)))))
(define cp (make-cpoint 3 'red 4))
(point-x cp) => 3
(point-y cp) => 4
(point-d cp) => 5
(cpoint-color cp) => red
最后来看看define-record-type的语法形式:
syntax: (define-record-type record-name clause …)
syntax: (define-record-type (record-name constructor pred) clause …)
Fields clause语法形式:
(fields field-spec …)
field-spec必须是下面5个中的一个:
- field-name
- (immmutable field-name)
- (mutable field-name)
- (immmutable field-name accessor-name)
- (mutable field-name accessor-name mutator-name)
define-record in Chez Scheme
Chez Scheme依然支持$R^6RS$中传统的record类型的定义,另外还提供一个新的语法,即define-record,其语法形式与define-record-type一样,所不同的地方在于,define-record中的字段默认是可变的,这与$R^6RS$中的record相反,因此,创建一个记录类型时,同时会定义下列过程:
(define-record point (x y))
(make-point x y) ;; constructor
(point? obj) ;; predicate
(point-x p) ;; accessor for field x
(point-y p) ;; accessor for field y
(set-point-x! p obj) ;; mutator for field x
(set-point-y! p obj) ;; mutator for field y
通过对define-record-type的了解,可以发现其非常OO,smalltalk是否受其影响不得而知,至少经过纵向比较对于我们学习技术大有裨益。