common-lisp Using Macros to define data structures


Example

A common use of macros is to create templates for data structures which obey common rules but may contain different fields. By writing a macro, you can allow the detailed configuration of the data structure to be specified without needing to repeat boilerplate code, nor to use a less efficient structure (such as a hash) in memory purely to simplify programming.

For example, suppose that we wish to define a number of classes which have a range of different properties, each with a getter and setter. In addition, for some (but not all) of these properties, we wish to have the setter call a method on the object notifying it that the property has been changed. Although Common LISP already has a shorthand for writing getters and setters, writing a standard custom setter in this way would normally require duplicating the code that calls the notification method in every setter, which could be a pain if there are a large number of properties involved. However, by defining a macro it becomes much easier:

(defmacro notifier (class slot) 
  "Defines a setf method in (class) for (slot) which calls the object's changed method."
   `(defmethod (setf ,slot) (val (item ,class))
     (setf (slot-value item ',slot) val)
     (changed item ',slot)))

(defmacro notifiers (class slots)
  "Defines setf methods in (class) for all of (slots) which call the object's changed method."
  `(progn 
     ,@(loop for s in slots collecting `(notifier ,class ,s))))

(defmacro defclass-notifier-slots (class nslots slots)  
  "Defines a class with (nslots) giving a list of slots created with notifiers, and (slots) giving a list of slots created with regular accessors."
  `(progn
     (defclass ,class () 
       ( ,@(loop for s in nslots collecting `(,s :reader ,s)) 
         ,@(loop for s in slots collecting `(,s :accessor ,s))))   
     (notifiers ,class ,nslots)))

We can now write (defclass-notifier-slots foo (bar baz qux) (waldo)) and immediately define a class foo with a regular slot waldo (created by the second part of the macro with the specification (waldo :accessor waldo)), and slots bar, baz, and qux with setters that call the changed method (where the getter is defined by the first part of the macro, (bar :reader bar), and the setter by the invoked notifier macro).

In addition to allowing us to quickly define multiple classes that behave this way, with large numbers of properties, without repetition, we have the usual benefit of code reuse: if we later decide to change how the notifier methods work, we can simply change the macro, and the structure of every class using it will change.