digikar
digikar

Reputation: 588

Separating initialization arguments and class slots in Common Lisp Object System for making objects

This asks about initializing slots from other slots. What I want to achieve instead is to take some arguments as input - perhaps but not necessarily to make-instance - and convert these arguments into the class slots for storage. In effect, I want to separate the implementation of the class from its (initialization) interface.

Is there a recommended way to achieve this?

The simplest way I can imagine is simply create a (defun make-my-object ...) as the interface. This may then call make-instance with appropriate arguments.

For example, imagine

(defclass my-object () (slot-1 slot-2))
(defun make-my-object (arg-1 arg-2)
  (make-instance 'my-object 
                 :slot-1 (+ arg-1 arg-2) 
                 :slot-2 (- arg-1 arg-2)))

Other ways I can imagine include implementing an initialize-instance :after that takes arg-1 and arg-2 as keyword arguments and initializes slot-1 and slot-2 appropriately. However, since after methods are called in the least-specific-first-order, that means that superclass slots will be initialized before current-class slot. On the other hand, what looks more common is that one will take arguments for constructing current-class, and on the basis of these arguments, one will initialize the super-class slots.

The alternative is initialize-instance :before - or also :around - but if multiple classes in the hierarchy have such "interface-implementation" differences, I don't see this working, unless I can pass arguments to call-next-method.

Are there other ways?

EDIT: Thanks to @ignis volens for bringing to my notice that (one of) my main concern(s) is about superclass slots being initialized from subclass slots. Is there a recommended way to do this?

Upvotes: 3

Views: 501

Answers (1)

ignis volens
ignis volens

Reputation: 9252

I am not sure I understand your question. The answer is almost certainly after methods on initialize-instance I think. You say that this will cause slots defined in superclasses to be initialized first: yes, it will, and that almost certainly what you want to happen. Slots defined in superclasses don't generally depend for their values on subclass slots (its always possible to think of exceptions to everything) and so initialising in least-specific first order is almost always what you want.

The two common ways of initializing slots that I use are either to simply declare what their initargs are in the definition:

(defclass minibeast ()
  ((legs :initform 'uncountable
         :initarg :legs
         :initarg :leg-count
         :accessor legs)
   (tentacles :initform 'many
              :initarg :tentacles
              :initarg :number-of-tentacles
              :accessor tentacles)))

And now (make-instance 'minibeast :legs 87) does what you expect. And this works (because, obviously it has to if the two slots were defined in different classes):

(defclass awful-monster ()
  ((legs :initform 'uncountable
         :initarg :legs
         :initarg :leg-count
         :accessor legs)
   (appendages :initform 'many
               :initarg :legs
               :initarg :appendages)))

Now (make-instance 'awful-monster :legs 93) will result in an awful monster with 93 legs and 93 appendages.

However that method perhaps doesn't qualify as separating interface from implementation. You may also want to perform some computation when initialising slots. In both these cases after methods on initialize-instance are generally the right approach:

(defclass horrible-monster ()
  ((legs :initform 983
         :accessor legs)
   (eyes :initform 63
         :accessor eyes)
   (appendages
    :reader appendages)))
   
(defmethod initialize-instance :after
  ((m horrible-monster) &key eyes legs (stalky-eyes t))
  (with-slots ((e eyes) (l legs) appendages) m
    (when eyes (setf e eyes))
    (when legs (setf l legs))
    (setf appendages (if stalky-eyes (+ e l) l))))

And now horrible monsters will get the appropriate number of appendages (I am not sure why horrible monsters don't know whether their eyes are on stalks or not: perhaps they don't have mirrors).

There are any number of other combinations of course. You might well not want to have user code call make-instance explicitly but wrap things up in some function:

(defun make-awful-thing (&rest args &key (sort-of-horrible-thing 'horrible-monster)
                               &allow-other-keys)
  (let ((the-remaining-args (copy-list args)))
    ;; No doubt alexandria or something has a way of doing this
    (remf the-remaining-args ':sort-of-horrible0thing)
    (apply #'make-instance sort-of-horrible-thing the-remaining-args)))

And now, of course you can have some bespoke initialzation protocol easily:

(defgeneric enliven-horrible-thing (horrible-thing &key)
  (:method :around ((horrible-thing t) &key)
   (call-next-method)
   t))

(defun make-awful-thing (&rest args &key (sort-of-horrible-thing 'horrible-monster)
                               &allow-other-keys)
  (let ((the-remaining-args (copy-list args)))
    ;; No doubt alexandria or something has a way of doing this
    (remf the-remaining-args ':sort-of-horrible0thing)
    (apply #'enliven-horrible-thing
           (apply #'make-instance sort-of-horrible-thing
                  the-remaining-args)
           the-remaining-args)))

(defmethod enliven-horrible-thing ((horrible-thing horrible-monster)
                                   &key (ichor t) (smell 'unspeakable))
  ...)

Upvotes: 4

Related Questions