17.7 新的实现 (New Implementation)
我们到目前为止所做的改善都是牺牲灵活性交换而来。在这个系统的开发后期,一个 Lisp 程序通常可以牺牲些许灵活性来获得好处,这里也不例外。目前为止我们使用哈希表来表示所有的对象。这给我们带来了超乎我们所需的灵活性,以及超乎我们所想的花费。在这个小节里,我们会重写我们的程序,用简单向量来表示对象。
(defun inst (parent)
(let ((obj (make-hash-table)))
(setf (gethash :parents obj) parent)
obj))
(defun rget (prop obj)
(let ((prec (gethash :preclist obj)))
(if prec
(dolist (c prec)
(multiple-value-bind (val in) (gethash prop c)
(if in (return (values val in)))))
(multiple-value-bind (val in) (gethash prop obj)
(if in
(values val in)
(rget prop (gethash :parents obj)))))))
(defun get-next (obj name)
(let ((prec (gethash :preclist obj)))
(if prec
(some #'(lambda (x) (gethash name x))
(cdr prec))
(get-next (gethash obj :parents) name))))
图 17.7: 定义实例
这个改变意味着放弃动态定义新属性的可能性。目前我们可通过引用任何对象,给它定义一个属性。现在当一个类别被创建时,我们会需要给出一个列表,列出该类有的新属性,而当实例被创建时,他们会恰好有他们所继承的属性。
在先前的实现里,类别与实例没有实际区别。一个实例只是一个恰好有一个父类的类别。如果我们改动一个实例的父类,它就变成了一个类别。在新的实现里,类别与实例有实际区别;它使得将实例转成类别不再可能。
在图 17.8-17.10 的代码是一个完整的新实现。图片 17.8 给创建类别与实例定义了新的操作符。类别与实例用向量来表示。表示类别与实例的向量的前三个元素包含程序自身要用到的信息,而图 17.8 的前三个宏是用来引用这些元素的:
(defmacro parents (v) `(svref ,v 0))
(defmacro layout (v) `(the simple-vector (svref ,v 1)))
(defmacro preclist (v) `(svref ,v 2))
(defmacro class (&optional parents &rest props)
`(class-fn (list ,@parents) ',props))
(defun class-fn (parents props)
(let* ((all (union (inherit-props parents) props))
(obj (make-array (+ (length all) 3)
:initial-element :nil)))
(setf (parents obj) parents
(layout obj) (coerce all 'simple-vector)
(preclist obj) (precedence obj))
obj))
(defun inherit-props (classes)
(delete-duplicates
(mapcan #'(lambda (c)
(nconc (coerce (layout c) 'list)
(inherit-props (parents c))))
classes)))
(defun precedence (obj)
(labels ((traverse (x)
(cons x
(mapcan #'traverse (parents x)))))
(delete-duplicates (traverse obj))))
(defun inst (parent)
(let ((obj (copy-seq parent)))
(setf (parents obj) parent
(preclist obj) nil)
(fill obj :nil :start 3)
obj))
图 17.8: 向量实现:创建
parents
字段取代旧实现中,哈希表条目里:parents
的位置。在一个类别里,parents
会是一个列出父类的列表。在一个实例里,parents
会是一个单一的父类。layout
字段是一个包含属性名字的向量,指出类别或实例的从第四个元素开始的设计 (layout)。preclist
字段取代旧实现中,哈希表条目里:preclist
的位置。它会是一个类别的优先级列表,实例的话就是一个空表。
因为这些操作符是宏,他们全都可以被 setf
的第一个参数使用(参考 10.6 节)。
class
宏用来创建类别。它接受一个含有其基类的选择性列表,伴随着零个或多个属性名称。它返回一个代表类别的对象。新的类别会同时有自己本身的属性名,以及从所有基类继承而来的属性。
> (setf *print-array* nil
gemo-class (class nil area)
circle-class (class (geom-class) radius))
#<Simple-Vector T 5 C6205E>
这里我们创建了两个类别: geom-class
没有基类,且只有一个属性, area
; circle-class
是 gemo-class
的子类,并添加了一个属性, radius
。 [1] circle-class
类的设计
> (coerce (layout circle-class) 'list)
(AREA RADIUS)
显示了五个字段里,最后两个的名称。 [2]
class
宏只是一个 class-fn
的介面,而 class-fn
做了实际的工作。它调用 inherit-props
来汇整所有新对象的父类,汇整成一个列表,创建一个正确长度的向量,并适当地配置前三个字段。( preclist
由 precedence
创建,本质上 precedence
没什么改变。)类别余下的字段设置为 :nil
来指出它们尚未初始化。要检视 circle-class
的 area
属性,我们可以:
> (svref circle-class
(+ (position 'area (layout circle-class)) 3))
:NIL
稍后我们会定义存取函数来自动办到这件事。
最后,函数 inst
用来创建实例。它不需要是一个宏,因为它仅接受一个参数:
> (setf our-circle (inst circle-class))
#<Simple-Vector T 5 C6464E>
比较 inst
与 class-fn
是有益学习的,它们做了差不多的事。因为实例仅有一个父类,不需要决定它继承什么属性。实例可以仅拷贝其父类的设计。它也不需要构造一个优先级列表,因为实例没有优先级列表。创建实例因此与创建类别比起来来得快许多,因为创建实例在多数应用里比创建类别更常见。
(declaim (inline lookup (setf lookup)))
(defun rget (prop obj next?)
(let ((prec (preclist obj)))
(if prec
(dolist (c (if next? (cdr prec) prec) :nil)
(let ((val (lookup prop c)))
(unless (eq val :nil) (return val))))
(let ((val (lookup prop obj)))
(if (eq val :nil)
(rget prop (parents obj) nil)
val)))))
(defun lookup (prop obj)
(let ((off (position prop (layout obj) :test #'eq)))
(if off (svref obj (+ off 3)) :nil)))
(defun (setf lookup) (val prop obj)
(let ((off (position prop (layout obj) :test #'eq)))
(if off
(setf (svref obj (+ off 3)) val)
(error "Can't set ~A of ~A." val obj))))
图 17.9: 向量实现:存取
现在我们可以创建所需的类别层级及实例,以及需要的函数来读写它们的属性。图 17.9 的第一个函数是 rget
的新定义。它的形状与图 17.7 的 rget
相似。条件式的两个分支,分别处理类别与实例。
- 若对象是一个类别,我们遍历其优先级列表,直到我们找到一个对象,其中欲找的属性不是
:nil
。如果没有找到,返回:nil
。 - 若对象是一个实例,我们直接查找属性,并在没找到时递回地调用
rget
。
rget
与 next?
新的第三个参数稍后解释。现在只要了解如果是 nil
, rget
会像平常那样工作。
函数 lookup
及其反相扮演着先前 rget
函数里 gethash
的角色。它们使用一个对象的 layout
,来取出或设置一个给定名称的属性。这条查询是先前的一个复本:
> (lookup 'area circle-class)
:NIL
由于 lookup
的 setf
也定义了,我们可以给 circle-class
定义一个 area
方法,通过:
(setf (lookup 'area circle-class)
#'(lambda (c)
(* pi (expt (rget 'radius c nil) 2))))
在这个程序里,和先前的版本一样,没有特别区别出方法与槽。一个“方法”只是一个字段,里面有着一个函数。这将很快会被一个更方便的前端所隐藏起来。
(declaim (inline run-methods))
(defmacro defprop (name &optional meth?)
`(progn
(defun ,name (obj &rest args)
,(if meth?
`(run-methods obj ',name args)
`(rget ',name obj nil)))
(defun (setf ,name) (val obj)
(setf (lookup ',name obj) val))))
(defun run-methods (obj name args)
(let ((meth (rget name obj nil)))
(if (not (eq meth :nil))
(apply meth obj args)
(error "No ~A method for ~A." name obj))))
(defmacro defmeth (name obj parms &rest body)
(let ((gobj (gensym)))
`(let ((,gobj ,obj))
(defprop ,name t)
(setf (lookup ',name ,gobj)
(labels ((next () (rget ,gobj ',name t)))
#'(lambda ,parms ,@body))))))
图 17.10: 向量实现:宏介面
图 17.10 包含了新的实现的最后部分。这个代码没有给程序加入任何威力,但使程序更容易使用。宏 defprop
本质上没有改变;现在仅调用 lookup
而不是 gethash
。与先前相同,它允许我们用函数式的语法来引用属性:
> (defprop radius)
(SETF RADIUS)
> (radius our-circle)
:NIL
> (setf (radius our-circle) 2)
2
如果 defprop
的第二个选择性参数为真的话,它展开成一个 run-methods
调用,基本上也没什么改变。
最后,函数 defmeth
提供了一个便捷方式来定义方法。这个版本有三件新的事情:它隐含了 defprop
,它调用 lookup
而不是 gethash
,且它调用 regt
而不是 278 页的 get-next
(译注: 图 17.7 的 get-next
)来获得下个方法。现在我们理解给 rget
添加额外参数的理由。它与 get-next
非常相似,我们同样通过添加一个额外参数,在一个函数里实现。若这额外参数为真时, rget
取代 get-next
的位置。
现在我们可以达到先前方法定义所有的效果,但更加清晰:
(defmeth area circle-class (c)
(* pi (expt (radius c) 2)))
注意我们可以直接调用 radius
而无须调用 rget
,因为我们使用 defprop
将它定义成一个函数。因为隐含的 defprop
由 defmeth
实现,我们也可以调用 area
来获得 our-circle
的面积:
> (area our-circle)
12.566370614359173