A small hack for the class redefinition

Tagged as lisp, hacks

Written on 2021-05-24 by Daniel 'jackdaniel' Kochmański

Consider the following class:

(defclass boring-class ()
  ((a :initarg :a :initform 'a :accessor a)
   (b :initarg :b :initform 'b :accessor b)
   (c :initarg :c :initform 'c :accessor c))
  (:default-initargs :c 'the-c))

(defmethod print-object ((instance boring-class) stream)
  (print-unreadable-object (instance stream :type nil :identity nil)
    (format stream "~a, ~a, ~a" (a instance) (b instance) (c instance))))

Now create an instance of it:

(defvar *boring-instance* (make-instance 'boring-class :b 'the-b))
(print *boring-instance*) ;;; #<A, THE-B, THE-C>

Redefining the class with different initial values has no effect on the instance local slots because they are not changed:

(defclass boring-class ()
  ((a :initarg :a :initform 'the-a :accessor a)
   (b :initarg :b :initform 'b :accessor b)
   (c :initarg :c :initform 'c :accessor c))
  (:default-initargs :c 'the-c))
  
(print *boring-instance*) ;;; #<A, THE-B, THE-C>

However if we define a new slot (casually - change its name) the CLOS machinery will update the instance:

(defclass boring-class ()
  ((X :initarg :a :initform 'new-a :accessor a)
   (Y :initarg :b :initform 'b :accessor b)
   (Z :initarg :c :initform 'new-c :accessor c))
  (:default-initargs :b 'new-b))
  
(print *boring-instance*) ;;; #<NEW-A, NEW-B, NEW-C>

In other words to trigger the slot update after each class redefinition we have to always provide a different name for the slot.

(defclass boring-class ()
  ((#.(gensym) :initarg :a :initform (random 42) :accessor a)
   (#.(gensym) :initarg :b :initform (random 42) :accessor b)
   (#.(gensym) :initarg :c :initform (random 42) :accessor c)))

(print *boring-instance*) ;;; #<15, 13, 18>
;; recompile defclass
(print *boring-instance*) ;;; #<3, 4, 38>
;; recompile defclass
(print *boring-instance*) ;;; #<32, 12, 10>

This is not the cleverest trick because we may have a better control over slots for example by defining the method for the function reinitialize-instance however I've found the use of gensyms surprising and funny. Cheers!

P.S. As pointed out by Michał Herda, using uninterned symbols will give us the same behavior and will preserve the symbol name of the slot name.

(defclass boring-class ()
  ((#:a :initarg :a :initform (random 42) :accessor a)
   (#:b :initarg :b :initform (random 42) :accessor b)
   (#:c :initarg :c :initform (random 42) :accessor c)))