defclassis a synonym for
define-structure, you won't be far off.
Here's a simple CLOS class definition:
(defclass test-class () ((name :initarg :name :initform 'zippy)))Objects of class
test-classhave a single field which is called
name. This class definition adds a method to
make-instancethat works as follows:
;; make an instance where the name is the symbol GRIFFY (defvar test-instance-1 (make-instance 'test-class :name 'griffy)) ;; alternatively, use the default name of ZIPPY (defvar test-instance-2 (make-instance 'test-class))In addition, we can use
slot-valueto extract the name:
(slot-value test-instance-1 'name) => GRIFFY (slot-value test-instance-2 'name) => ZIPPYAnd that's that. (And you thought CLOS was complicated.)
A persistent class is defined as follows:
(defclass test-class () ((name :initarg :name :initform 'zippy)) (:metaclass persistent-standard-class) (:schema-version 0))The
:metaclassspecifier tells CLOS that we want instances to be persistent. The
:schema-versionis a bit of magic that protects against the unlikely, but possible nasty case that over time a class definition might evolve where a slot name is accidentally re-used. In such a case, the code will need to disambiguate which class definition was intended and it can use the
:schema-version. You can safely ignore this by following this simple rule: If you ever modify the slots in a persistent class, you should increment the
So what is different? On the surface, practically nothing changes. The calls to
slot-valuework just as before. There are four changes that happen ‘under the covers’
- The object returned by
make-instancewill be a persistent object.
- Two additional slots are allocated. The first contains the OID of the instance itself, the second contains the persistent store where the instance was allocated.
nameslot in the object will contain the OID of the persistent name rather than a Lisp object.
make-instanceis modified to extract the OIDs of its arguments to initialize this slot.
slot-valuemust be modified to dereference the OID that is actually in the slot.
We use the meta-object protocol to override
(defmethod clos:slot-value-using-class ((class persistent-standard-class) (object persistent-standard-object) slot) (persistent-object/find (persistent-standard-object/pstore object) (call-next-method)))The call to
call-next-methodinvokes the underlying mechanism for reading a slot value. But we're not storing the actual slot value in the slot, we are storing the OID of the actual slot value, so
call-next-methodwill return that OID. We look up the returned OID in the persistent store to get the value. (In order to support multiple persistent stores, we need to know which store contains the value. We require that all subfields of an object be allocated in the same persistent store as the containing object.)
The modifications to
make-instanceare more complex. We actually don't modify
make-instancedirectly, but instead we modify
clos:shared-initialize(which is invoked by
make-instanceas part of the instance allocation protocol).
;; Override the primary shared-instance method ;; in order to deal with persistent slots. (defmethod clos:shared-initialize ((instance persistent-standard-object) slot-names &rest initargs &key persistent-store oid &allow-other-keys) ;; We have to wrap the initargs and initforms ;; in persistent-objects and create an initializer ;; for this object. (let* ((class (class-of instance)) (init-plist (compute-persistent-slot-initargs class (or persistent-store *default-persistent-store*) initargs)) (oid (persistent-object/save (make-initializer class (class-schema-version class) init-plist) (or persistent-store *default-persistent-store*) oid))) (apply #'call-next-method instance slot-names (nconc init-plist initargs)) (setf (persistent-standard-object/oid instance) oid) instance)) (defun compute-persistent-slot-initargs (class persistent-store initargs) "Scan over the persistent effective slots in CLASS, determine the value to be assigned to each slot, either from the initargs or from the initfunction, then using the persistent-initarg as a key, construct a plist for use in the persistent initializer and in the inner call to shared-initialize." (let ((result nil)) (iterate (((slot-initargs slot-initfunction slot-persistent-initarg) (map-fn '(values t t symbol) #'effective-slot-initialization-info (scan-class-persistent-effective-slots class)))) (let ((initial-value (slot-initial-value initargs slot-initargs slot-initfunction (clos::slot-unbound-value)))) (unless (eq initial-value (clos::slot-unbound-value)) (push slot-persistent-initarg result) (push (slot-value->persistent-node persistent-store initial-value) result)))) (nreverse result)))In
compute-persistent-slot-initargs, you can see the call to
slot-value->persistent-nodethat extracts the OID from the initarg. In
shared-initialize, the call to
persistent-object/savewrites an initialization record to the store and then allocates and initializes the transient, in-memory version of the object.
These are the essential changes to the MOP to implement the basic persistence mechanism.
More details to come in the next post.