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.