WITH-SLOTS and WITH-ACCESSORS

While using accessor functions will make your code easier to maintain, they can still be a bit verbose. And there will be times, when writing methods that implement the low-level behaviors of a class, that you may specifically want to access slots directly to set a slot that has no writer function or to get at the slot value without causing any auxiliary methods defined on the reader function to run.

This is what **SLOT-VALUE** is for; however, it’s still quite verbose. To make matters worse, a function or method that accesses the same slot several times can become clogged with calls to accessor functions and **SLOT-VALUE**. For example, even a fairly simple method such as the following, which assesses a penalty on a bank-account if its balance falls below a certain minimum, is cluttered with calls to balance and **SLOT-VALUE**:

  1. (defmethod assess-low-balance-penalty ((account bank-account))
  2. (when (< (balance account) *minimum-balance*)
  3. (decf (slot-value account 'balance) (* (balance account) .01))))

And if you decide you want to directly access the slot value in order to avoid running auxiliary methods, it gets even more cluttered.

  1. (defmethod assess-low-balance-penalty ((account bank-account))
  2. (when (< (slot-value account 'balance) *minimum-balance*)
  3. (decf (slot-value account 'balance) (* (slot-value account 'balance) .01))))

Two standard macros, **WITH-SLOTS** and **WITH-ACCESSORS**, can help tidy up this clutter. Both macros create a block of code in which simple variable names can be used to refer to slots on a particular object. **WITH-SLOTS** provides direct access to the slots, as if by **SLOT-VALUE**, while **WITH-ACCESSORS** provides a shorthand for accessor methods.

The basic form of **WITH-SLOTS** is as follows:

  1. (with-slots (slot*) instance-form
  2. body-form*)

Each element of slots can be either the name of a slot, which is also used as a variable name, or a two-item list where the first item is a name to use as a variable and the second is the name of the slot. The instance-form is evaluated once to produce the object whose slots will be accessed. Within the body, each occurrence of one of the variable names is translated to a call to **SLOT-VALUE** with the object and the appropriate slot name as arguments.10 Thus, you can write assess-low-balance-penalty like this:

  1. (defmethod assess-low-balance-penalty ((account bank-account))
  2. (with-slots (balance) account
  3. (when (< balance *minimum-balance*)
  4. (decf balance (* balance .01)))))

or, using the two-item list form, like this:

  1. (defmethod assess-low-balance-penalty ((account bank-account))
  2. (with-slots ((bal balance)) account
  3. (when (< bal *minimum-balance*)
  4. (decf bal (* bal .01)))))

If you had defined balance with an :accessor rather than just a :reader, then you could also use **WITH-ACCESSORS**. The form of **WITH-ACCESSORS** is the same as **WITH-SLOTS** except each element of the slot list is a two-item list containing a variable name and the name of an accessor function. Within the body of **WITH-ACCESSORS**, a reference to one of the variables is equivalent to a call to the corresponding accessor function. If the accessor function is **SETF**able, then so is the variable.

  1. (defmethod assess-low-balance-penalty ((account bank-account))
  2. (with-accessors ((balance balance)) account
  3. (when (< balance *minimum-balance*)
  4. (decf balance (* balance .01)))))

The first balance is the name of the variable, and the second is the name of the accessor function; they don’t have to be the same. You could, for instance, write a method to merge two accounts using two calls to **WITH-ACCESSORS**, one for each account.

  1. (defmethod merge-accounts ((account1 bank-account) (account2 bank-account))
  2. (with-accessors ((balance1 balance)) account1
  3. (with-accessors ((balance2 balance)) account2
  4. (incf balance1 balance2)
  5. (setf balance2 0))))

The choice of whether to use **WITH-SLOTS** versus **WITH-ACCESSORS** is the same as the choice between **SLOT-VALUE** and an accessor function: low-level code that provides the basic functionality of a class may use **SLOT-VALUE** or **WITH-SLOTS** to directly manipulate slots in ways not supported by accessor functions or to explicitly avoid the effects of auxiliary methods that may have been defined on the accessor functions. But you should generally use accessor functions or **WITH-ACCESSORS** unless you have a specific reason not to.