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.