Skip to content

LISP - Slots and Accessors

Welcome to the Common Lisp Object System (CLOS), one of the most powerful and flexible object systems available. In this chapter, we’ll learn the fundamentals: defining classes, the slots that hold their data, and the accessors used to interact with that data.

A class is a blueprint for creating objects. We define a class using the defclass macro. Inside, we define slots, which are the data members of the class. Let’s model a product for an e-commerce application.

(defclass product ()
((id :initarg :id
:reader product-id)
(name :initarg :name
:accessor product-name)
(price :initarg :price
:accessor product-price
:initform 0.0
:type 'float)
(inventory-count :initarg :inventory
:accessor product-inventory
:initform 0
:type 'integer)))

Each slot is defined with a name followed by options that control its behavior:

  • :initarg: Creates a keyword (e.g., :id) that can be used to initialize this slot when creating an object with make-instance.
  • :accessor: Creates a generic function (e.g., product-name) that can both read the slot’s value and write to it using (setf (product-name an-instance) new-value).
  • :reader: Creates a function (e.g., product-id) that can only read the slot’s value. This is useful for immutable fields like an ID.
  • :writer: (Not used above) Creates a function that can only write to the slot. It’s less common than :accessor or :reader.
  • :initform: Provides a default value for the slot if it’s not supplied during initialization (e.g., price defaults to 0.0).
  • :type: Specifies the expected type of data for the slot. This helps with error checking and potential compiler optimizations.

We create an instance of a class with make-instance. We then use the accessor and reader functions we defined to interact with its data.

;; Define the 'product' class as shown above
;; --- Create an instance of the class ---
(defvar *keyboard*
(make-instance 'product
:id "KBD-42"
:name "Mechanical Keyboard"
:price 99.99
:inventory 50))
;; --- Read data using accessors and readers ---
(format t "--- Initial Product State ---~%")
(format t "ID: ~a (read-only)~%" (product-id *keyboard*))
(format t "Name: ~a~%" (product-name *keyboard*))
(format t "Price: $~,2f~%" (product-price *keyboard*))
(format t "In Stock: ~d~%~%" (product-inventory *keyboard*))
;; --- Update data using an accessor ---
(format t "--- Updating Product State ---~%")
(format t "Putting the keyboard on sale...~%")
(setf (product-price *keyboard*) 79.99)
;; A customer buys one
(decf (product-inventory *keyboard*))
;; --- Display the updated state ---
(format t "New Price: $~,2f~%" (product-price *keyboard*))
(format t "New Stock: ~d~%" (product-inventory *keyboard*))
;; --- Trying to write to a read-only slot will cause an error ---
; (setf (product-id *keyboard*) "NEW-ID") ; This would signal an error

When you execute the code, it returns the following result:

--- Initial Product State ---
ID: KBD-42 (read-only)
Name: Mechanical Keyboard
Price: $99.99
In Stock: 50
--- Updating Product State ---
Putting the keyboard on sale...
New Price: $79.99
New Stock: 49
  • Encapsulation: Using :reader for immutable data and :accessor for mutable data is a key part of encapsulation, protecting your objects from invalid state changes.
  • make-instance: This is the universal constructor for all CLOS objects. The use of keyword arguments makes initialization code readable and self-documenting.
  • The Power of CLOS: This is just the beginning. The real power of CLOS comes from generic functions and defmethod, which allow you to define behavior that is specialized for different classes, enabling true polymorphism. For example, you could define a generic ship-item function that behaves differently for a physical-product versus a digital-download.