I use a macro to create some classes and map strings to them in a global variable:
(defvar *labels-to-classes* (make-hash-table :test 'equal))
(defmacro define-labeled-class (name label slots)
`(progn
(defclass ,name ()
,slots)
(setf (gethash ,label *labels-to-classes*) (find-class ',name))))
I have another class whose instances know how to make instances of these labeled classes. It uses the global hashtable.
(defclass factory ()
((x :accessor factory-x)))
(defmethod make-instance-from-label ((fact factory) label)
(let ((o (make-instance (gethash label *labels-to-classes*))))
;; use values in fact to set up o
o))
I think the map from strings to classes would be better located in a slot in the factory class with class allocation. But how do I fill it up when I'm defining the other classes, before I have even one instance of it?
As said by Barmar in comments, you can use CLASS-PROTOTYPE
. Here is an example with no custom macros, just regular MOP:
(ql:quickload :closer-mop)
;; define a metaclass: instances of this type are classes with
;; additional class slots.
(defclass labeled-class (c2mop:standard-class)
(;; the label associated with this class
(label :accessor label :initarg :label)
;; all classes whose metaclass is labeled-class share the same
;; cache mapping labels to classes.
(cache :allocation :class
:initform (make-hash-table :test #'equal))))
;; this is necessary to be able the inherit from a standard class
(defmethod c2mop:validate-superclass ((o labeled-class) (_ standard-class)) t)
;; when defining a class whose metaclass is labeled-class, store it in
;; the cache under its label
(defmethod initialize-instance :after ((c labeled-class) &key &allow-other-keys)
"Map label of c to c in the global cache"
(setf (gethash (label c) (slot-value c 'cache)) c))
For example, I define two labeled classes. Note how the :label
argument is given as a class option; I wrote (:label . "foo")
with a dot so that the argument is the string foo, not the list ("foo")
(that would be the case with (:label "foo")
due to how class options are defined).
(defclass my-foo-class ()
((x :initform 0 :reader x))
(:label . "foo")
(:metaclass labeled-class))
(defclass my-bar-class ()
((y :initform 0 :reader y))
(:label . "bar")
(:metaclass labeled-class))
Using the class prototype of labeled-class
, it is possible to access the shared cache
value:
(alexandria:hash-table-plist
(slot-value (c2mop:class-prototype (find-class 'labeled-class))
'cache))
("foo" #<LABELED-CLASS COMMON-LISP-USER::MY-FOO-CLASS>
"bar" #<LABELED-CLASS COMMON-LISP-USER::MY-BAR-CLASS>)