4

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?)

Joshua Taylor
  • 84,998
  • 9
  • 154
  • 353
mobiuseng
  • 2,326
  • 1
  • 16
  • 30
  • 3
    You can also take an instance of A and change its class to B - then initializing the added slots. Note that you really need to get rid of the 'A implements methods' thinking. This is CLOS, where classes/slots and generic functions/methods are a bit more independent. – Rainer Joswig Feb 03 '16 at 11:39
  • @RainerJoswig It's interesting idea of changing the class. But I'm not sure if I fully understand you on relation between classes/slots and generic functions/methods. I realize they are quite independent entities. What would be an alternative to 'A implements method' thinking? – mobiuseng Feb 03 '16 at 11:51
  • 3
    @mobiuseng in clos, it's not that "a implements method f",is that "there's a method defined on the generic function f specialized for a". It's a more noticeable difference when you're specializing methods on multiple arguments. – Joshua Taylor Feb 03 '16 at 12:20
  • 1
    Joshua is right. Plus: "there ARE methodS defined on the generic function f specialized for a'. It could be more than one. – Rainer Joswig Feb 03 '16 at 12:38
  • @RainerJoswig Ok, I see. It's just that in this particular case I specialized just over one argument, so it looked like "implementing the method". CLOS's way shifts the focus quite a bit. – mobiuseng Feb 03 '16 at 13:15

2 Answers2

4

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?

Dirk
  • 30,623
  • 8
  • 82
  • 102
  • Thanks for the answer! I didn't think about non-trivial initialization... I am trying to decide the relationships between the building blocks to compose them properly. Thus, I thought of this idea. So far it seems that composition will work better than inheritance. – mobiuseng Feb 03 '16 at 11:43
3

What you are trying to do can be done using composition as a form of prototypal inheritance where an object "inherits" from another instance.

(defclass prototype-mixin ()
  ((parent :initarg :parent :initform nil :accessor parent)))

(defmethod slot-unbound (c (p prototype-mixin) slot)
  (declare (ignore c))
  (let ((parent (parent p)))
    (if parent
      (slot-value parent slot)
      (call-next-method))))

Now, you define two classes:

(defclass a ()
  ((slot :initarg :slot)))

(defclass b (a prototype-mixin) 
  ((other :initarg :other)))

When you create a b from an existing instance of a, you set the parent slot of b to a. Since b is also an a, there is an unbound slot in b. When you try to access this slot, you access the one present in the "parent" object, which is an instance of a. But if you want, you can override the value in b.

This approach is inspired by a post from Erik Naggum on comp.lang.lisp.

coredump
  • 37,664
  • 5
  • 43
  • 77