The Common Lisp Object System (CLOS) is pretty powerful. It gives you multiple inheritance, multiple dispatch, and many different ways to extend the behavior of methods. Underneath, most implementations use the Metaobject Protocol (MOP), a way of defining CLOS in terms of itself. As part of the MOP, classes are implemented as objects with several instance variables. Among those are variables that hold the class’s name, its superclasses, and a list of the class’s own instance variables. If you don’t believe me, take the point class from the previous post:
(defclass point () ((x :accessor point-x :initarg :x :initform 0) (y :accessor point-y :initarg :y :initform 0)))
And use the Slime Inspector to inspect the point class object, which can be obtained by calling find-class:
The advantage of using the MOP is that it makes it possible to fine tune the behavior of CLOS by using ordinary object-oriented programming. A great example of this is the filtered-functions library which adds arbitrary predicate based dispatch to CLOS. But enough about the MOP.1 In this post I’m going to talk about one tiny piece of CLOS, update-instance-for-redefined-class.
Update-instance-for-redefined-class is a method which is called whenever a class is redefined (at runtime). By overriding it, you can customize what exactly happens at that point in time. For example, let’s say you are using the above point class to represent complex numbers for some sort of simulation. As part of the simulation, you have a point object saved inside of the *location* variable:
After profiling the simulation, you find that one of the bottlenecks is complex multiplication. Since multiplication of complex numbers is much more efficient when they are represented in polar form, you decide that you want to change the implementation of the point class from Cartesian to polar coordinates. To do that (at runtime), all you need to do is run the following code:
(defmethod update-instance-for-redefined-class :before ((pos point) added deleted plist &key) (let ((x (getf plist 'x)) (y (getf plist 'y))) (setf (point-rho pos) (sqrt (+ (* x x) (* y y))) (point-theta pos) (atan y x)))) (defclass point () ((rho :initform 0 :accessor point-rho) (theta :initform 0 :accessor point-theta))) (defmethod point-x ((pos point)) (with-slots (rho theta) pos (* rho (cos theta)))) (defmethod point-y ((pos point)) (with-slots (rho theta) pos (* rho (sin theta))))
Basically, the code extends update-instance-for-redefined-class to calculate the values of rho and theta for the polar implementation in terms of the variables x and y from the Cartesian one. After extending update-instance-for-redefined-class the code then redefines the class, causing all of the existing instances to be changed over to the new implementation.2 Finally, two methods are defined, point-x and point-y, which preserve the interface for the point class.3 After running the code and then inspecting the contents of *location*, you should see:
Even though the object inside of *location* is still the same object, it is now implemented using polar coordinates! To make sure that it was converted from Cartesian to polar correctly, you decide to call point-x on the object to check that the x-coordinate is still the same:
Amazingly, all of the code continues to work even though the implementation of an entire class was completely changed. So anytime you want to change the implementation of a class that is part of a service that needs to be up 24/7 and just happens to be written in Common Lisp, remember to use update-instance-for-redefined-class.
- If you are interested in learning more about the MOP, look for a copy of “The Art of the Metaobject Protocol”. Alan Kay, the creator of object-oriented programming, called it “The best book anybody has written in ten years” in his 1997 OOPSLA talk.
- Actually the time at which update-instance-for-redefined-class is called is unspecified. The only guarantee is that it will be called on an instance before a variable of that instance is accessed.
- There are several other functions that need to be redefined such as initialize-instance in order to truly preserve the interface.