root/trunk/bknr/datastore/src/indices/tutorial.lisp

Revision 3373, 23.8 kB (checked in by hans, 1 week ago)

remove extra parens in indices tutorial

  • Property svn:eol-style set to native
  • Property svn:keywords set to author date id revision
Line 
1 ;;; Slot indexes for Common Lisp
2
3 ;;;# Introduction
4 ;;;
5 ;;; In the framework we built as a backend for the `eboy.com' website,
6 ;;; we built a prevalence layer that could handle CLOS objects. These
7 ;;; CLOS objects all had an `ID', and could be indexed over other
8 ;;; slots as well. For example, we heavily used "keyword indices" that
9 ;;; could give back all objects that had a certain keyword stored in a
10 ;;; slot. However, the slot indices were built into a very big
11 ;;; `define-persistent-class' macro, and could not easily be extended
12 ;;; or used on their own.
13 ;;;
14 ;;; This index layer is now built using the metaobject protocol, and
15 ;;; has a CLOS method protocol to access indices, so that new index
16 ;;; classes can easily be added.
17 ;;;
18 ;;; This tutorial will show you how to create CLOS classes with slot
19 ;;; indices, class indices, and how to create custom indices and use
20 ;;; them with your classes.
21
22
23 ;;;# Obtaining and loading BKNR slot indices
24 ;;;
25 ;;; You can obtain the current CVS sources of BKNR by following the
26 ;;; instructions at `http://bknr.net/'. Add the `src' directory of
27 ;;; BKNR to your `asdf:*central-registry*', and load the indices
28 ;;; module by evaluating the following form:
29
30 (asdf:oos 'asdf:load-op :bknr.indices)
31
32 ;;; Then switch to the `bknr.indices' package to try out the tutorial.
33
34 (in-package :bknr.indices)
35
36 ;;;# A simple indexed class
37 ;;;
38 ;;;## A standard non-indexed class
39 ;;;
40 ;;; We begin by defining a simple class called GORILLA. Gorillas have
41 ;;; a name, and a description keyword.
42
43 (defclass gorilla ()
44   ((name        :initarg :name
45                 :reader gorilla-name
46                 :type string)
47    (description :initarg :description
48                 :reader gorilla-description)))
49
50 (defmethod print-object ((gorilla gorilla) stream)
51   (print-unreadable-object (gorilla stream :type t)
52     (format stream "~S" (gorilla-name gorilla))))
53  
54
55 ;;; We can create a few gorillas to test the class. To refer to these
56 ;;; gorillas later on, we have to store them in a list. We can then
57 ;;; write functions to search for gorillas.
58
59 (defvar *gorillas* nil)
60
61 (setf *gorillas*
62       (list
63        (make-instance 'gorilla :name "Lucy"
64                       :description :aggressive)
65        (make-instance 'gorilla :name "Robert"
66                       :description :playful)
67        (make-instance 'gorilla :name "John"
68                       :description :aggressive)))
69
70 (defun all-gorillas ()
71   (copy-list *gorillas*))
72
73 (defun gorilla-with-name (name)
74   (find name *gorillas* :test #'string-equal
75         :key #'gorilla-name))
76
77 (defun gorillas-with-description (description)
78   (remove description *gorillas* :test-not #'eql :key
79           #'gorilla-description))
80
81 (all-gorillas)
82 ; => (#<GORILLA "Lucy"> #<GORILLA "Robert"> #<GORILLA "John">)
83 (gorilla-with-name "Lucy")
84 ; => #<GORILLA "Lucy">
85 (gorillas-with-description :aggressive)
86 ; => (#<GORILLA "Lucy"> #<GORILLA "John">)
87 (gorilla-with-name "Manuel")
88 ; => NIL
89
90 ;;; What we would like to do however, is have the object system index
91 ;;; these objects for us. This is achieved by using INDEXED-CLASS as
92 ;;; the metaclass for the gorilla class. The `INDEXED-CLASS' has its
93 ;;; own slot-definition objects called `INDEX-DIRECT-SLOT-DEFINITION'
94 ;;; and `INDEX-EFFECTIVE-SLOT-DEFINITION'. Using these classes, we can
95 ;;; specify additional initargs to our slot definitions.
96 ;;;
97 ;;;## Additional slot initargs
98 ;;;
99 ;;; The following additional initargs are available:
100 ;;;
101 ;;; `INDEX' - A class name that specifies the class of the index to
102 ;;; use. For example `UNIQUE-INDEX', `HASH-INDEX' or
103 ;;; `HASH-LIST-INDEX'.
104 ;;;
105 ;;; `INDEX-INITARGS' - Additional arguments that are passed to
106 ;;; `INDEX-INITIALIZE' when creating the index.
107 ;;;
108 ;;; `INDEX-READER' - A symbol under which a query function for the
109 ;;; index will be stored.
110 ;;;
111 ;;; `INDEX-KEYS' - A symbol under which a function returning all the
112 ;;; values of the index will be stored.
113 ;;;
114 ;;; `INDEX-SUBCLASSES' - Determines if instances of subclasses of this
115 ;;; class will be indexed in the slot index also. Defaults to `T'.
116 ;;;
117 ;;;## A simple indexed class
118 ;;;
119 ;;; Using the `INDEXED-CLASS', we can redefine our gorilla example.
120
121 ;;; Before we are able to refine GORILLA with a new metaclass, we need
122 ;;; to delete the old class definition:
123
124 (setf (find-class 'gorilla) nil)
125
126 (defclass gorilla ()
127   ((name :initarg :name :reader gorilla-name
128          :index-type unique-index
129          :index-initargs (:test #'equal)
130          :index-reader gorilla-with-name
131          :index-values all-gorillas)
132    (description :initarg :description
133                 :reader gorilla-description
134                 :index-type hash-index
135                 :index-reader gorillas-with-description))
136   (:metaclass indexed-class))
137
138 (defmethod print-object ((gorilla gorilla) stream)
139   (print-unreadable-object (gorilla stream :type t)
140     (format stream "~S" (gorilla-name gorilla))))
141
142 ;;; We have to recreate the gorillas though, as the old instances
143 ;;; don't get updated for now.
144
145 (make-instance 'gorilla :name "Lucy" :description :aggressive)
146 (make-instance 'gorilla :name "Robert" :description :playful)
147 (make-instance 'gorilla :name "John" :description :aggressive)
148
149 (all-gorillas)
150 ; => (#<GORILLA "Lucy"> #<GORILLA "Robert"> #<GORILLA "John">)
151 (gorilla-with-name "Lucy")
152 ; => #<GORILLA "Lucy">
153 ; T
154 (gorillas-with-description :aggressive)
155 ; => (#<GORILLA "John"> #<GORILLA "Lucy">)
156 ; T
157
158 ;;;## Class indices
159 ;;;
160 ;;; We can also create indices that are not bound to a single
161 ;;; slot. These indices are called `CLASS-INDICES'. For example, we
162 ;;; can add two slots for the coordinates of the gorilla, and a class
163 ;;; index of type `ARRAY-INDEX' that will index the two slots `X' and
164 ;;; `Y' of the gorilla in an array of dimensions `256x256'. Note that
165 ;;; redefining the class conserves the existing indices.
166
167 (defclass gorilla ()
168   ((name :initarg :name :reader gorilla-name
169          :index-type unique-index
170          :index-initargs (:test #'equal)
171          :index-reader gorilla-with-name
172          :index-values all-gorillas)
173    (description :initarg :description
174                 :reader gorilla-description
175                 :index-type hash-index
176                 :index-reader gorillas-with-description)
177    (x :initarg :x :reader gorilla-x)
178    (y :initarg :y :reader gorilla-y))
179   (:metaclass indexed-class)
180   (:class-indices (coords :index-type array-index
181                           :slots (x y)
182                           :index-reader gorilla-with-coords
183                           :index-initargs (:dimensions '(256 256)))))
184
185 (make-instance 'gorilla :name "Pete" :description
186                :playful :x 5 :y 8)
187
188 (gorilla-with-coords '(5 8))
189 ; => #<GORILLA "Pete">
190 (all-gorillas)
191 ; => (#<GORILLA "Lucy"> #<GORILLA "Robert">
192 ;     #<GORILLA "John"> #<GORILLA "Pete">)
193 (gorillas-with-description :playful)
194 ; => (#<GORILLA "Pete"> #<GORILLA "Robert">)
195 ; T
196
197 (let ((lucy (gorilla-with-name "Lucy")))
198   (with-slots (x y) lucy
199     (setf x 0 y 0)))
200
201 (gorilla-with-name "Lucy")
202 ; => #<GORILLA "Lucy">
203 ; T
204 (gorilla-with-coords '(0 0))
205 ; => #<GORILLA "Lucy">
206
207 ;;;# Creating indexed classes
208 ;;;
209 ;;; Adding indexes to a class is very simple. The class has to have
210 ;;; the metaclass `INDEXED-CLASS', or a class deriving from
211 ;;; `INDEXED-CLASS'.
212 ;;;
213
214 ;;;## Slot indices
215 ;;;
216 ;;; `INDEXED-CLASS' uses its own `EFFECTIVE-SLOT-DEFINITION' and
217 ;;; `DIRECT-SLOT-DEFINITION' which add indices to slots. A slot
218 ;;; definition in the `DEFCLASS' form supports additional keyword
219 ;;; arguments:
220 ;;;
221 ;;; `:INDEX' - Specifies an existing index to use as slot-index for this slot.
222 ;;;
223 ;;; `:INDEX-TYPE' - Specifies the class of the index to be used for this
224 ;;; slot.
225 ;;;
226 ;;; `:INDEX-INITARGS' - Specifies additional initargs to be given to
227 ;;; `INDEX-CREATE' when creating the index. The slot-name is given as
228 ;;; the `:SLOT' keyword argument to `INDEX-CREATE'.
229 ;;;
230 ;;; `:INDEX-READER' - Specifies the name under which a query function
231 ;;; for the created index will be saved.
232 ;;;
233 ;;; `:INDEX-VALUES' - Specifies the name under which a function returning
234 ;;; all the objects stored in the created index will be saved.
235 ;;;
236 ;;; `:INDEX-MAPVALUES' - Specifies the name under which a function
237 ;;; applying a function to all the objects stored in the created index
238 ;;; will be saved.
239 ;;;
240 ;;; `:INDEX-SUBCLASSES' - Specifies if subclasses of the class will
241 ;;; also be indexing in this index. Default is `T'.
242 ;;;
243 ;;; For each `DIRECT-SLOT-DEFINITION' of an indexed class with the
244 ;;; `:INDEX' keyword, an index is created and stored in the
245 ;;; `DIRECT-SLOT-DEFINITION'. All the direct indexes are then stored
246 ;;; in the `EFFECTIVE-SLOT-DEFINITION' (indexes with
247 ;;; `INDEX-SUBCLASSES = NIL' will not).
248 ;;;
249 ;;; Every access to the slot will update the indices stored in the
250 ;;; `EFFECTIVE-SLOT-DEFINITION'. When the slot is changed, the object
251 ;;; is removed from all the slot indices, and added after the slot
252 ;;; value has been changed. When a slot is made unbound, the object is
253 ;;; removed from the slot indices.
254
255 (defclass test-slot ()
256   ((a :initarg :a :index-type unique-index
257       :reader test-slot-a
258       :index-reader test-slot-with-a
259       :index-values all-test-slots)
260    (b :initarg :b :index-type unique-index
261       :index-reader test-slot-with-b
262       :index-subclasses nil
263       :index-values all-test-slots-bs))
264   (:metaclass indexed-class))
265
266 (defclass test-slot2 (test-slot)
267   ((b :initarg :b :index-type unique-index
268       :index-reader test-slot2-with-b
269       :index-subclasses nil
270       :index-mapvalues map-test-slot2s
271       :index-values all-test-slot2s-bs))
272   (:metaclass indexed-class))
273
274 (defmethod print-object ((object test-slot) stream)
275   (print-unreadable-object (object stream :type t)
276     (format stream "~S" (test-slot-a object))))
277
278 (make-instance 'test-slot :a 1 :b 2)
279 (make-instance 'test-slot :a 2 :b 3)
280 (make-instance 'test-slot2 :a 3 :b 4)
281 (make-instance 'test-slot2 :a 4 :b 2)
282 (make-instance 'test-slot2 :a 5 :b 9)
283
284 (all-test-slots)
285 ; => (#<TEST-SLOT 1> #<TEST-SLOT 2> #<TEST-SLOT2 3>
286 ;     #<TEST-SLOT2 4> #<TEST-SLOT2 5>)
287 (test-slot-with-a 2)
288 ; => #<TEST-SLOT 2>
289 (all-test-slots-bs)
290 ; => (#<TEST-SLOT 1> #<TEST-SLOT 2>)
291 (all-test-slot2s-bs)
292 ; (#<TEST-SLOT2 3> #<TEST-SLOT2 4> #<TEST-SLOT2 5>)
293 (map-test-slot2s (lambda (obj) (print obj)))
294 ;
295 ; #<TEST-SLOT2 3>
296 ; #<TEST-SLOT2 4>
297 ; #<TEST-SLOT2 5>
298 ;
299 ; NIL
300
301 ;;; Here is an example of a slot index using an already existing index.
302
303 (defvar *existing-unique-index*
304   (index-create 'unique-index :slots '(a)))
305
306 (defclass test-slot3 ()
307   ((a :initarg :a :index *existing-unique-index*))
308   (:metaclass indexed-class))
309
310 (make-instance 'test-slot3 :a 3)
311 (make-instance 'test-slot3 :a 4)
312
313 (index-get *existing-unique-index* 4)
314 ; => #<TEST-SLOT3 {493B9655}>
315 ; T
316 (index-values *existing-unique-index*)
317 ; => (#<TEST-SLOT3 {493A0CBD}> #<TEST-SLOT3 {493B9655}>)
318
319 ;;; The slot indices of a class can be examined using
320 ;;; `CLASS-SLOT-INDICES'.
321
322 (class-slot-indices (find-class 'test-slot) 'a)
323 ; => (#<UNIQUE-INDEX SLOT: A SIZE: 5 {599FA9F5}>)
324 (class-slot-indices (find-class 'test-slot) 'b)
325 ; => (#<UNIQUE-INDEX SLOT: B SIZE: 2 {59A038BD}>)
326 (class-slot-indices (find-class 'test-slot2) 'a)
327 ; => (#<UNIQUE-INDEX SLOT: A SIZE: 5 {599FA9F5}>)
328 (class-slot-indices (find-class 'test-slot2) 'b)
329 ; => (#<UNIQUE-INDEX SLOT: B SIZE: 3 {59A0D6A5}>)
330
331 ;;; Note that a slot can have multiple indices.
332
333 (defclass test-slot4 (test-slot)
334   ((a :initarg :a :index-type unique-index
335       :index-reader test-slot4-with-a
336       :index-values all-test-slot4s))
337   (:metaclass indexed-class))
338
339 (make-instance 'test-slot4 :a 6 :b 9)
340
341 (all-test-slots)
342 ; => (#<TEST-SLOT 1> #<TEST-SLOT 2> #<TEST-SLOT2 3>
343 ;     #<TEST-SLOT2 4> #<TEST-SLOT2 5>
344 ;     #<TEST-SLOT4 6>)
345 (all-test-slot4s)
346 ; => (#<TEST-SLOT4 6>)
347 (class-slot-indices (find-class 'test-slot4) 'a)
348 ; => (#<UNIQUE-INDEX SLOT: A SIZE: 6 {599FA9F5}>
349 ;  #<UNIQUE-INDEX SLOT: A SIZE: 1 {59079E25}>)
350
351
352 ;;;## Class indices
353 ;;;
354 ;;; In addition to slot indices, an indexed class supports class
355 ;;; indices which react when one of several slots is changing. For
356 ;;; example, in the `GORILLA' class above, the `COORDS' index reacts
357 ;;; on slots `X' and `Y'. By default, a class index reacts on all
358 ;;; slots.
359 ;;;
360 ;;; A class index is created by adding a class option `CLASS-INDICES'
361 ;;; followed by a list of class index specifications.
362
363 (defclass test-class ()
364   ((x :initarg :x :reader test-class-x)
365    (y :initarg :y :reader test-class-y)
366    (z :initarg :z :reader test-class-z))
367   (:metaclass indexed-class)
368   (:class-indices (2d-coords :index-type array-index :slots (x y)
369                              :index-initargs (:dimensions '(256 256))
370                              :index-reader test-with-2d-coords)
371                   (3d-coords :index-type array-index :slots (x y z)
372                              :index-reader test-with-3d-coords
373                              :index-initargs (:dimensions '(256 256 2)))))
374
375 (defmethod print-object ((object test-class) stream)
376   (print-unreadable-object (object stream :type t)
377     (with-slots (x y z) object
378       (format stream "~d,~d,~d" x y z))))
379
380 (make-instance 'test-class :x 1 :y 1 :z 0)
381 (make-instance 'test-class :x 1 :y 3 :z 1)
382 (make-instance 'test-class :x 1 :y 2 :z 0)
383
384 (test-with-3d-coords '(1 1 0))
385 ; => #<TEST-CLASS 1,1,0>
386 (test-with-2d-coords '(1 1))
387 ; => #<TEST-CLASS 1,1,0>
388 (test-with-2d-coords '(1 2))
389 ; => #<TEST-CLASS 1,2,0>
390
391 ;;; A class index specification has to comply with the following
392 ;;; lambda-list `(NAME &REST ARGS &KEY INDEX-READER INDEX-VALUES SLOTS
393 ;;; TYPE INDEX &ALLOW-OTHER-KEYS)'. The key arguments `:INDEX-TYPE',
394 ;;; `:INDEX', `:INDEX-READER' and `:INDEX-VALUES' are then removed from
395 ;;; the initargs, and the rest is passed to `INDEX-CREATE' to create
396 ;;; the class index.
397 ;;;
398 ;;; `:INDEX-TYPE' - specifies the type of the class index.
399 ;;;
400 ;;; `:INDEX' - (optional) specifies an already existing index object
401 ;;; to use.
402 ;;;
403 ;;; `:INDEX-READER' - Like `:INDEX-READER' for slot
404 ;;; indices.
405 ;;;
406 ;;; `:INDEX-VALUES' - Like `:INDEX-VALUES' for slot indices.
407 ;;;
408 ;;; Using `:INDEX', we can use already existing indices as class
409 ;;; indices.
410
411 (defvar *array-index*
412   (index-create 'array-index :slots '(x y z)
413                 :dimensions '(256 256 2)))
414
415 (defclass test-class2 (test-class)
416   ()
417   (:metaclass indexed-class)
418   (:class-indices (coords :index *array-index* :slots (x y z)
419                           :index-reader test-with-coords)))
420
421 (make-instance 'test-class2 :x 5 :y 5 :z 0)
422
423 *array-index*
424 ; => #<ARRAY-INDEX SLOTS: (X Y Z) ((256 256 2)) {593F383D}>
425 (index-get *array-index* '( 5 5 0))
426 ; => #<TEST-CLASS2 5,5,0>
427 (test-with-coords '(5 5 0))
428 ; => #<TEST-CLASS2 5,5,0>
429
430 ;;; XXX the class index tutorial needs updating, please skip to next section!
431
432 ;;; Another example of a class index is the `CLASS-INDEX' index.
433
434 (defvar *class-index* (index-create 'class-index))
435
436 (defclass base-object ()
437   ()
438   (:metaclass indexed-class)
439   (:class-indices (class :index *class-index*
440                          :slots nil
441                          :index-reader objects-of-class
442                          :index-values all-objects
443                          :index-subclasses t
444                          :index-keys all-class-names)
445                   (classes :index-type class-index
446                            :index-initargs (:index-superclasses t)
447                            :slots nil
448                            :index-subclasses t
449                            :index-reader objects-with-class)))
450
451 (defclass child1 (base-object)
452   ()
453   (:metaclass indexed-class))
454
455 (defclass child2 (base-object)
456   ((a :initarg :a))
457   (:metaclass indexed-class))
458
459 (make-instance 'child1)
460 (make-instance 'child1)
461 (make-instance 'child1)
462 (make-instance 'child2)
463 (make-instance 'child2)
464
465 (all-objects)
466 ; => (#<CHILD1 {48E5CB3D}> #<CHILD1 {48E51395}> #<CHILD1 {48E453DD}>
467 ;  #<CHILD2 {48E82F55}> #<CHILD2 {48E7746D}>)
468 (objects-with-class 'child1)
469 ; => (#<CHILD1 {48E5CB3D}> #<CHILD1 {48E51395}> #<CHILD1 {48E453DD}>)
470 ; T
471 (objects-with-class 'child2)
472 ; => (#<CHILD2 {48E82F55}> #<CHILD2 {48E7746D}>)
473 ; T
474 (objects-with-class 'base-object)
475 ; => (#<CHILD2 {48E82F55}> #<CHILD2 {48E7746D}> #<CHILD1 {48E5CB3D}>
476 ;  #<CHILD1 {48E51395}> #<CHILD1 {48E453DD}>)
477 ; T
478 (objects-of-class 'child1)
479 ; => (#<CHILD1 {48E5CB3D}> #<CHILD1 {48E51395}> #<CHILD1 {48E453DD}>)
480 ; T
481 (objects-of-class 'child2)
482 ; => (#<CHILD2 {48E82F55}> #<CHILD2 {48E7746D}>)
483 ; T
484 (objects-of-class 'base-object)
485 ; => NIL
486 ; NIL
487
488 ;;;## Destroying objects
489 ;;;
490 ;;; Indexed objects will not be garbage collected until they are
491 ;;; removed from the indices. This is done by calling the
492 ;;; `DESTROY-OBJECT' method on the object. This removes the object
493 ;;; from all its indices, and sets the slot `DESTROYED-P' to `T', so
494 ;;; that not slot-access is possible anymore on the object.
495
496 (make-instance 'test-class2 :x 5 :y 5 :z 0)
497
498 (let ((obj (test-with-coords '(5 5 0))))
499   (destroy-object obj)
500
501 ;;; This will throw an error:
502 ;;;   Can not get slot X of destroyed object of class TEST-CLASS.
503
504   (test-class-x obj))
505
506 ;;;## Class and object reinitialization
507 ;;;
508 ;;; When a class is redefined, the indexed-class code tries to map
509 ;;; the new slot-indices to the old-indices. If it finds a slot-index
510 ;;; in the old `EFFECTIVE-SLOT-DEFINITION' and a slot-index in the new
511 ;;; `EFFECTIVE-SLOT-DEFINITION', it calls `INDEX-REINITIALIZE' on the
512 ;;; two indices to copy the values form the old index to the new
513 ;;; one. Afterwards, the same is done for the class
514 ;;; indices. `INDEX-REINITIALIZE' will not be called with the
515 ;;; old-index being the same as the new-index, so that explicitly
516 ;;; instantiated class indices don't get reinitialized with
517 ;;; themselves.
518 ;;;
519 ;;; Indices for new slots or new class indices are obviously empty on
520 ;;; creation, and will be filled when the existing instances are
521 ;;; updated. For now, `SHARED-INITIALIZE' is not overloaded, so the
522 ;;; instance updates are noticed through `(SETF SLOT-VALUE-USING-CLASS)'.
523
524 ;;;# Creating a custom index
525 ;;;
526 ;;; The main reason to write indexed slots was to be able to use
527 ;;; custom indices that are appropriate for the task at hand. Indices
528 ;;; are CLOS objects that follow the index method protocol. The
529 ;;; methods that have to be implemented are:
530 ;;;
531 ;;; `INDEX-ADD (INDEX OBJECT)' - Add OBJECT to the INDEX. Throws an
532 ;;; ERROR if a problem happened while inserting OBJECT."
533 ;;;
534 ;;; `INDEX-GET (INDEX KEY)' - Get the object (or the objects) stored
535 ;;; under the index-key KEY.
536 ;;;
537 ;;; `INDEX-REMOVE (INDEX OBJECT)' - Remove OBJECT from the INDEX.
538 ;;;
539 ;;; `INDEX-KEYS (INDEX)' - Returns all the keys of the index.
540 ;;;
541 ;;; `INDEX-VALUES (INDEX)' - Returns all the objects stored in INDEX.
542 ;;;
543 ;;; `INDEX-REINITIALIZE (NEW-INDEX OLD-INDEX)' - Called when the
544 ;;; definition of an index is changed.
545 ;;;
546 ;;; `INDEX-CLEAR (INDEX)' - Remove all indexed objects from the index.
547 ;;;
548 ;;; In addition to this method, there is the function `INDEX-CREATE'
549 ;;; that instantiates an index object, and calls `INDEX-INITIALIZE' on
550 ;;; it.
551 ;;;
552 ;;; The best way to see how this methods are used is to have at look
553 ;;; at the basic index `SLOT-INDEX'. A unique index indexes an object
554 ;;; under a key stored in a slot of this object, so a slot index is
555 ;;; initialized using two arguments: the slot-name where the key is
556 ;;; stored, and a test to create the underlying hash-table.
557
558 (defclass slot-index ()
559   ((hash-table :initarg :hash-table :accessor slot-index-hash-table
560                :documentation "The internal hash table used to index
561 objects.")
562    (slot-name :initarg :slot-name :reader slot-index-slot-name
563               :documentation "The value of the slot with name
564 SLOT-NAME is used as a key to the internal hash-table.")
565    (index-nil :initarg :index-nil :reader slot-index-index-nil
566               :initform nil
567               :documentation "If T, NIL is used as a valid slot
568  value, else slots with NIL value are treated as unbound slots.")))
569
570 (defmethod initialize-instance :after ((index slot-index) &key (test #'eql) slots index-nil)
571   (unless (<= (length slots) 1)
572     (error "Can not create slot-index with more than one slot."))
573   (with-slots (hash-table slot-name) index
574     (setf hash-table (make-hash-table :test test)
575           slot-name (first slots)
576           (slot-value index 'index-nil) index-nil)))
577
578 ;;; When a class is redefined, the indices are re-created. However, we
579 ;;; still want our existing objects to be indexed by the new index,
580 ;;; therefore `INDEX-REINITIALIZE' copies the hash-table when the
581 ;;; hash-table test is the same, or else copies all the stored objects
582 ;;; into the new hash-table.
583
584 (defmethod index-reinitialize ((new-index slot-index)
585                                (old-index slot-index))
586   "Reinitialize the slot-bound index from the old index by copying the
587 internal hash-table if the hash-table test is the same, or by
588 iterating over the values of the old-table and reentering them into
589 the new hash-table."
590   (let ((new-hash (slot-index-hash-table new-index))
591         (old-hash (slot-index-hash-table old-index)))
592     (if (eql (hash-table-test new-hash)
593              (hash-table-test old-hash))
594         (setf (slot-index-hash-table new-index)
595               old-hash)
596         (loop for key being the hash-keys of old-hash
597               using (hash-value value)
598               do (setf (gethash key new-hash) value)))
599     new-index))
600
601 ;;; `INDEX-CLEAR' just creates an empty hash-table to replace the
602 ;;; existing hash-table.
603
604 (defmethod index-clear ((index slot-index))
605   (with-slots (hash-table) index
606     (setf hash-table (make-hash-table
607                       :test (hash-table-test hash-table)))))
608
609 ;;; `INDEX-ADD' and `INDEX-REMOVE' both use the slot-name to get the
610 ;;; key value, and use this key to query the underlying
611 ;;; hash-table. `INDEX-ADD' is not defined for the base class
612 ;;; `SLOT-INDEX', however it is defined in the simple child class
613 ;;; `UNIQUE-INDEX'. When another object is stored under the key, an
614 ;;; error is thrown.
615
616 (defclass unique-index (slot-index)
617   ())
618
619 (defmethod index-add ((index unique-index) object)
620   "Add an object using the value of the specified slot as key.
621 When the hash-table entry already contains a value, an error
622 is thrown."
623   (unless (slot-boundp object (slot-index-slot-name index))
624     (return-from index-add))
625   (let* ((key (slot-value object (slot-index-slot-name index)))
626          (hash-table (slot-index-hash-table index)))
627     (when (and (not (slot-index-index-nil index))
628                (null key))
629       (return-from index-add))
630     (multiple-value-bind (value presentp)
631         (gethash key hash-table)
632       (when (and presentp
633                  (not (eql value object)))
634         (error (make-condition 'index-existing-error
635                                :index index :key key :value value)))
636       (setf (gethash key hash-table) object))))
637
638 (defmethod index-remove ((index slot-index) object)
639   (let ((slot-name (slot-index-slot-name index)))
640     (if (slot-boundp object slot-name)
641         (remhash (slot-value object slot-name)
642                  (slot-index-hash-table index))
643         (warn "Ignoring request to remove object ~a
644 with unbound slot ~A."
645               object slot-name))))
646
647 ;;; The rest of the methods are straightforward.
648
649 (defmethod index-get ((index slot-index) key)
650   (gethash key (slot-index-hash-table index)))
651
652 (defmethod index-keys ((index slot-index))
653   (loop for key being the hash-keys
654         of (slot-index-hash-table index)
655         collect key))
656
657 (defmethod index-values ((index slot-index))
658   (loop for value being the hash-values
659         of (slot-index-hash-table index)
660         collect value))
661
662 ;;;# Creating an index using multiple slots
663 ;;;
664 ;;; When creating an index using multiple slots, you have to take care
665 ;;; of a few things. It can happen that a slot-value used by the index
666 ;;; is updated, but that the other slots that are needed are
667 ;;; unbound. However, this is not always an error, so a class index
668 ;;; has to check that all the slots it needs are bound. This is the
669 ;;; `INDEX-ADD' method for an array index.
670
671 (defmethod index-add ((index array-index) object)
672   (let* ((slot-values
673           (mapcar #'(lambda (slot-name)
674                       ;; return when not all slots are set
675                       ;;
676                       ;; - 18.10.04 not needed because of
677                       ;; make-instance around method
678                       ;;
679                       ;; - 19.10.04 in fact this is needed because
680                       ;; when adding a class index, the existing
681                       ;; instances are not reinitailized using
682                       ;; make-instnace, so we have to catch this...
683                       (unless (slot-boundp object slot-name)
684                                     (return-from index-add nil))
685                                   (slot-value object slot-name))
686                               (array-index-slot-names index)))
687          (array (array-index-array index))
688          (dimensions (array-dimensions array)))
689     (loop for slot-value in slot-values
690           for dimension in dimensions
691           when (>= slot-value dimension)
692           do (error "Could not add ~a to array-index ~a
693 because the coordinates ~a are out of bound."
694                     object index slot-values))
695     (let ((value (apply #'aref array slot-values)))
696       (when (and value
697                  (not (eql value object)))
698         (error (make-condition 'index-existing-error
699                                :index index :key slot-values
700                                :value value))))
701     (setf (apply #'aref array slot-values)
702           object)))
703
Note: See TracBrowser for help on using the browser.