Object Initialization with Slot Dependencies

Tags: lisp, Date: 2009-07-04

Consider a class with a trivial initialization dependency between slots A and B:

(defclass super ()
 ((a :initarg :a :reader a)
  (b :initform 0 :initarg :b :reader b)))

(defmethod initialize-instance :after ((super super) &key &allow-other-keys)
 (setf (slot-value super 'a) (1+ (slot-value super 'b))))

(a (make-instance 'super)) => 1
(a (make-instance 'super :b 1)) => 2

You may even subclass it, add an initform, and it still works:

(defclass sub (super)
 ((b :initform 1)))

(a (make-instance 'sub)) => 2

The complication begins when a subclass adds another slot, C, from which B is to be computed:

(defclass sub2 (super)
 ((c :initarg :c)))

Say, B is to be initialized to C + 1. That's easy, let's just add an after method, but the after method of the subclass runs after the after method of the superclass, hence the value of A is wrong:

(defmethod initialize-instance :after ((sub2 sub2) &key c &allow-other-keys)
 (setf (slot-value sub2 'b) (1+ c)))

(a (make-instance 'sub2 :c 1)) => 1

Sure, it should be a before method. And the previous example is now fixed:

(defmethod initialize-instance :before ((sub2 sub2) &key c &allow-other-keys)
 (setf (slot-value sub2 'b) (1+ c)))

(a (make-instance 'sub2 :c 1)) => 3

However, it doesn't work if SUB2 or a subclass of it has an initform on C:

(defclass sub3 (sub2)
 ((c :initform 2 :initarg :c)))

(a (make-instance 'sub3)) => error

because C is not passed as an initarg for which the before method is unprepared. At this point, one can say screw initforms and use DEFAULT-INITARGS, but that's fragile in the face of unsuspecting subclasses breaking this convention. Alternatively, one can initialize C early, which handles both initforms and initargs fine:

(defclass sub4 (super)
 ((c :initform 2 :initarg :c)))

(defmethod initialize-instance :around ((sub4 sub4) &rest initargs
                                       &key &allow-other-keys)
 (apply #'shared-initialize sub4 '(c) initargs)
 (apply #'call-next-method sub4 :b (1+ (slot-value sub4 'c)) initargs))

(a (make-instance 'sub4)) => 4
(a (make-instance 'sub4 :c 10)) => 12

That's the best I could come up with; educate me if you have a better idea.

UPDATE: Lazy initialiation has been suggested as an alternative. However, a naive implementation based on SLOT-BOUND-P is bound to run into problems when the lazily computed slot has an initform in a superclass. With SLOT-VALUE-USING-CLASS, one can probably mimick most of the semantics here in a very clean manner, but to avoid recomputing the value on every access, additional bookkeeping is needed, again, due to initforms.