lispcommon-lispclos

In Lisp CLOS, how can a class setter automatically update another slot?


I am new to CLOS. Here is my example:

   (defclass box ()
    ((length :accessor box-length :initform 0 :initarg :bxl)
     (breath :accessor box-breadth :initform 0 :initarg :bxb)
     (height :accessor box-height :initform 0 :initarg :bxh)
     (volume :reader   volume     :initform 0 :initarg :v)))

And the constructor is:

    (defun make-box (l b h)
     (make-instance 'box :bxl l :bxb b :bxh h :v (* l b h)))

So when I make an instance of the 'box' like this:

    ; make a box, 4 x 3 x 2
    (defparameter my-box (make-box 4 3 2))`

It works as I expected. I can 'describe' my-box and get:

    (describe my-box)
      #<BOX {100363F493}>
        [standard-object]

    Slots with :INSTANCE allocation:
      LENGTH                         = 4
      BREATH                         = 3
      HEIGHT                         = 2
      VOLUME                         = 24

Now, the question. If I update the 'height' like this:

    (setf (box-height my-box) 5)

How can I make this 'setf' automatically update the 'volume' slot?

So that VOLUME would change to (* 4 3 5) = 60?


Solution

  • One way to do this is an after method on the setf method of the various accessors. So:

    (defmethod (setf box-length) :after (length (b box))
      (with-slots (breadth height volume) b
        (setf volume (* length breadth height))))
    

    This could be done by a before method as well, but if you use a general 'update-the-volume' function you want to use an after method to avoid storing the slots twice, or define the setf side of the accessor completely yourself.

    Another approach which is certainly simpler is to not have a volume slot at all but compute it:

    (defclass box ()
      ((length :accessor box-length :initform 0 :initarg :bxl)
       (breath :accessor box-breadth :initform 0 :initarg :bxb)
       (height :accessor box-height :initform 0 :initarg :bxh)))
    
    (defgeneric volume (object))
    
    (defmethod volume ((b box))
      (* (box-length b) (box-breadth b) (box-height b)))
    

    Obviously other classes can still have a volume slot and methods on the volume generic function can access that slot: the protocol is the same.

    You can even make describe report the volume, either by defining a method on describe-object for boxes, or just defining an after method. In the latter case in particular you probably have to fiddle to get the formatting to agree with whatever your implementation's describe does. Here is a method which coincidentally is OK for my usual implementation (LispWorks):

    (defmethod describe-object :after ((b box) stream)
      (format stream "~& and volume ~D~%" (volume b)))
    

    Now

    > (describe (make-instance 'box))
    
    #<box 801001147B> is a box
    length      0
    breath      0
    height      0
     and volume 0