Search code examples
lispcommon-lispclos

How can I pass a super-class object to a sub-class constructor?


Let's say I have class A with a couple of slots:

(defclass a ()
  ((a-1 :initarg :a-1)
   (a-2 :initarg :a-2)))

And class B that inherits from A:

(defclass b (a)
  ((b-1 :initarg :b-1)))

If I want to instantiate B, make-instance will offer me slots :a-1, :a-2 and :b-1.

Here is a crazy idea: what if I want to instantiate B using the existing instance of A and only filling slot b-1?

PS. Why it can be useful: if A implements some generic methods that B inherits directly, without adding anything new. In alternative approach, making instance of A to be a slot in B, I would need to write trivial method wrappers to invoke these methods on that slot.

The only way I can think of: in auxiliary constructor decompose object A and pass corresponding slots to make-instance for B, i.e.:

(defun make-b (b-1 a-obj)
  (with-slots (a-1 a-2) a-obj
    (make-instance 'b :b-1 b-1 :a-1 a-1 :a-2 a-2)))

Are there better ways of doing this? (or perhaps, this approach leads to very bad design and I should avoid it altogether?)


Solution

  • I don't think, that there is a general solution. Consider: what should happen, for example, if class A has some slots, which are not simply initialized from some :initarg, but, say, during initialize-instance or shared-initialize?

    That said, as long as you are in control of all involved classes, you could try

    • make a protocol implemented by A, something along the lines of

      (defgeneric initargs-for-copy (object)
        (:method-combination append)
        (:method append (object) nil))
      
      (defmethod initargs-for-copy append ((object a))
        (list :a-1 (slot-value object 'a-1) :a-2 (slot-value object 'a-2)))
      
      (defun make-b (b-1 a-obj)
        (apply #'make-instance 'b :b-1 b-1 (initargs-for-copy a-obj)))
      
    • use the MOP to extract the slots at run-time (this may require knowledge about the Lisp implementation of your choice, or the help of some library, like closer-mop available via quicklisp)

      (defun list-init-args (object)
        (let* ((class (class-of object))
               (slots (closer-mop:class-slots class)))
          (loop
            for slot in slots
            for name = (closer-mop:slot-definition-name slot)
            for keyword = (closer-mop:slot-definition-initargs slot)
            when (and keyword (slot-boundp object name))
              nconc (list (car keyword) (slot-value object name)))))
      
      (defun make-b (b-1 a-obj)
         (apply #'make-instance 'b :b-1 b-1 (list-init-args a-obj)))
      
    • use change-class to transmogrify the A instance into a B instance destructively.

    Regardless: I am not sure, whether your use-case actually warrants inheritance. The composition approach seems (from a design point of view) much clearer here. Besides having B inherit some generic method implementations via A: are instances of B really considered to be proper instances of A in your actual application (i.e., is there an is-a? relationship)? Or are you just trying to avoid having to provide the wrappers here?