Object Initialization with Slot Dependencies
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
a 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 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.