Object Initialization

Since you can’t do much with an object with unbound slots, it’d be nice to be able to create objects with their slots already initialized. Common Lisp provides three ways to control the initial value of slots. The first two involve adding options to the slot specifier in the **DEFCLASS** form: with the :initarg option, you can specify a name that can then be used as a keyword parameter to **MAKE-INSTANCE** and whose argument will be stored in the slot. A second option, :initform, lets you specify a Lisp expression that will be used to compute a value for the slot if no :initarg argument is passed to **MAKE-INSTANCE**. Finally, for complete control over the initialization, you can define a method on the generic function **INITIALIZE-INSTANCE**, which is called by **MAKE-INSTANCE**.5

A slot specifier that includes options such as :initarg or :initform is written as a list starting with the name of the slot followed by the options. For example, if you want to modify the definition of bank-account to allow callers of **MAKE-INSTANCE** to pass the customer name and the initial balance and to provide a default value of zero dollars for the balance, you’d write this:

  1. (defclass bank-account ()
  2. ((customer-name
  3. :initarg :customer-name)
  4. (balance
  5. :initarg :balance
  6. :initform 0)))

Now you can create an account and specify the slot values at the same time.

  1. (defparameter *account*
  2. (make-instance 'bank-account :customer-name "John Doe" :balance 1000))
  3. (slot-value *account* 'customer-name) ==> "John Doe"
  4. (slot-value *account* 'balance) ==> 1000

If you don’t supply a :balance argument to **MAKE-INSTANCE**, the **SLOT-VALUE** of balance will be computed by evaluating the form specified with the :initform option. But if you don’t supply a :customer-name argument, the customer-name slot will be unbound, and an attempt to read it before you set it will signal an error.

  1. (slot-value (make-instance 'bank-account) 'balance) ==> 0
  2. (slot-value (make-instance 'bank-account) 'customer-name) ==> error

If you want to ensure that the customer name is supplied when the account is created, you can signal an error in the initform since it will be evaluated only if an initarg isn’t supplied. You can also use initforms that generate a different value each time they’re evaluated—the initform is evaluated anew for each object. To experiment with these techniques, you can modify the customer-name slot specifier and add a new slot, account-number, that’s initialized with the value of an ever-increasing counter.

  1. (defvar *account-numbers* 0)
  2. (defclass bank-account ()
  3. ((customer-name
  4. :initarg :customer-name
  5. :initform (error "Must supply a customer name."))
  6. (balance
  7. :initarg :balance
  8. :initform 0)
  9. (account-number
  10. :initform (incf *account-numbers*))))

Most of the time the combination of :initarg and :initform options will be sufficient to properly initialize an object. However, while an initform can be any Lisp expression, it has no access to the object being initialized, so it can’t initialize one slot based on the value of another. For that you need to define a method on the generic function **INITIALIZE-INSTANCE**.

The primary method on **INITIALIZE-INSTANCE** specialized on **STANDARD-OBJECT** takes care of initializing slots based on their :initarg and :initform options. Since you don’t want to disturb that, the most common way to add custom initialization code is to define an :after method specialized on your class.6 For instance, suppose you want to add a slot account-type that needs to be set to one of the values :gold, :silver, or :bronze based on the account’s initial balance. You might change your class definition to this, adding the account-type slot with no options:

  1. (defclass bank-account ()
  2. ((customer-name
  3. :initarg :customer-name
  4. :initform (error "Must supply a customer name."))
  5. (balance
  6. :initarg :balance
  7. :initform 0)
  8. (account-number
  9. :initform (incf *account-numbers*))
  10. account-type))

Then you can define an :after method on **INITIALIZE-INSTANCE** that sets the account-type slot based on the value that has been stored in the balance slot.7

  1. (defmethod initialize-instance :after ((account bank-account) &key)
  2. (let ((balance (slot-value account 'balance)))
  3. (setf (slot-value account 'account-type)
  4. (cond
  5. ((>= balance 100000) :gold)
  6. ((>= balance 50000) :silver)
  7. (t :bronze)))))

The **&key** in the parameter list is required to keep the method’s parameter list congruent with the generic function’s—the parameter list specified for the **INITIALIZE-INSTANCE** generic function includes **&key** in order to allow individual methods to supply their own keyword parameters but doesn’t require any particular ones. Thus, every method must specify **&key** even if it doesn’t specify any **&key** parameters.

On the other hand, if an **INITIALIZE-INSTANCE** method specialized on a particular class does specify a **&key** parameter, that parameter becomes a legal parameter to **MAKE-INSTANCE** when creating an instance of that class. For instance, if the bank sometimes pays a percentage of the initial balance as a bonus when an account is opened, you could implement that using a method on **INITIALIZE-INSTANCE** that takes a keyword argument to specify the percentage of the bonus like this:

  1. (defmethod initialize-instance :after ((account bank-account)
  2. &key opening-bonus-percentage)
  3. (when opening-bonus-percentage
  4. (incf (slot-value account 'balance)
  5. (* (slot-value account 'balance) (/ opening-bonus-percentage 100)))))

By defining this **INITIALIZE-INSTANCE** method, you make :opening-bonus-percentage a legal argument to **MAKE-INSTANCE** when creating a bank-account object.

  1. CL-USER> (defparameter *acct* (make-instance
  2. 'bank-account
  3. :customer-name "Sally Sue"
  4. :balance 1000
  5. :opening-bonus-percentage 5))
  6. *ACCT*
  7. CL-USER> (slot-value *acct* 'balance)
  8. 1050