A Comprehensive Guide to CLOS (Common Lisp Object System)
Welcome to a comprehensive guide on Object-Oriented Programming in Common Lisp using CLOS (the Common Lisp Object System). While Lisp is often associated with functional programming, CLOS provides one of the most powerful and flexible object systems ever designed. CLOS was standardized as part of ANSI Common Lisp in 1994 and influenced the design of object systems in many other languages.
This document explores how Common Lisp approaches OOP concepts and provides practical examples using SBCL (Steel Bank Common Lisp). By understanding CLOS, you'll be able to write modular, maintainable, and highly flexible code that takes advantage of features like multiple inheritance, generic functions, and method combinations.
After completing this exercise, you will be able to:
defclass including slots and accessorsmake-instance and custom constructor functionsdefgeneric and defmethod:include option in class definitionsObject-Oriented Programming is a paradigm that organizes code around "objects" that contain both data (variables) and functions (methods). Think of an object like a blueprint: a Drink class defines what properties all drinks have and what operations can be performed on them.
The core principles of OOP are:
CLOS supports all of these principles, but approaches them differently than languages like Java or C++. The key differences are:
Here's how CLOS compares to other popular object-oriented languages:
| Feature | Java | C++ | Common Lisp (CLOS) |
|---|---|---|---|
| Class/Type Definition | class ClassName |
class ClassName |
(defclass class-name ...) |
| Instance Creation | new ClassName() |
new ClassName() |
(make-instance 'class-name) |
| Method Definition | Inside class body | Inside class body | (defmethod name ...) (outside class) |
| Method Dispatch | Single dispatch (on this) |
Single dispatch (on this) |
Multiple dispatch (on any/all arguments) |
| Inheritance | Single (+ interfaces) | Multiple | Multiple |
| Access Modifiers | public, private, protected |
public, private, protected |
By convention and accessor functions |
| Memory Management | Automatic (GC) | Manual or smart pointers | Automatic (GC) |
| This/Self Reference | this |
this |
Explicit parameter in method |
Let's start with a simple class definition. In CLOS, we use defclass to define a class and its slots (instance variables):
; drink-basic.lisp
; Basic class definition in CLOS
;; ============================================================
;; CLASS DEFINITION
;; ============================================================
;; DEFCLASS defines a new class
;; Syntax: (defclass class-name (superclasses) (slot-definitions) options)
(defclass drink ()
;; The empty () means no superclasses (other than the default T)
;; Slot definitions - these are like instance variables
((name
;; :initarg - keyword used when creating instances
:initarg :name
;; :initform - default value if not specified
:initform "Unknown"
;; :accessor - creates both getter and setter methods
:accessor drink-name)
(cost
:initarg :cost
:initform 1.75
:accessor drink-cost)
;; A constant-like slot (we won't provide a setter)
(maximum
:initform 10
;; :reader - creates only a getter (no setter)
:reader drink-maximum)))
;; ============================================================
;; MAIN PROGRAM
;; ============================================================
(defun main ()
;; Create an instance using MAKE-INSTANCE
;; :name and :cost are the :initarg keywords we defined
(let ((my-drink (make-instance 'drink
:name "Coke"
:cost 1.75)))
;; Access slots using the accessor functions
(format t "Drink name: ~a~%" (drink-name my-drink))
(format t "Drink cost: $~,2f~%" (drink-cost my-drink))
(format t "Maximum capacity: ~a~%" (drink-maximum my-drink))
;; Modify a slot using the accessor (setf form)
(setf (drink-cost my-drink) 2.00)
(format t "New cost: $~,2f~%" (drink-cost my-drink))))
(main)
Run with:
sbcl --script drink-basic.lisp
Output:
Drink name: Coke
Drink cost: $1.75
Maximum capacity: 10
New cost: $2.00
:initarg: Keyword argument name used with make-instance:initform: Default value if :initarg not provided:accessor: Creates both reader and writer functions:reader: Creates only a reader function (read-only)Here are the most commonly used slot options in CLOS:
| Option | Description | Example |
|---|---|---|
:initarg |
Keyword used when creating instances | :initarg :name |
:initform |
Default value expression | :initform 0 |
:accessor |
Creates reader and writer functions | :accessor drink-name |
:reader |
Creates only a reader function | :reader drink-maximum |
:writer |
Creates only a writer function | :writer set-drink-name |
:documentation |
Documentation string for the slot | :documentation "The drink's name" |
:type |
Expected type (for documentation/optimization) | :type string |
In CLOS, methods are defined separately from classes using defgeneric and defmethod. This is the key difference from Java/C++ style OOP:
; drink-methods.lisp
; Demonstrating generic functions and methods in CLOS
;; ============================================================
;; CLASS DEFINITION
;; ============================================================
(defclass drink ()
((name
:initarg :name
:initform "Unknown"
:accessor drink-name)
(cost
:initarg :cost
:initform 1.75
:accessor drink-cost)
(available
:initform 0
:accessor drink-available)
(consumed
:initform 0
:accessor drink-consumed)
(money-made
:initform 0.0
:accessor drink-money-made)
(maximum
:initform 10
:reader drink-maximum)))
;; ============================================================
;; GENERIC FUNCTION DECLARATIONS (optional but recommended)
;; ============================================================
;; DEFGENERIC declares a generic function
;; This is optional - DEFMETHOD will create it automatically
;; But it's good practice for documentation
(defgeneric vend (drink)
(:documentation "Dispense a drink if available."))
(defgeneric refill (drink)
(:documentation "Restock the drink to maximum capacity."))
(defgeneric profit (drink)
(:documentation "Return the total money made from this drink."))
;; ============================================================
;; METHOD DEFINITIONS
;; ============================================================
;; DEFMETHOD defines a method that specializes on a class
;; The (drink drink) means: parameter 'drink' must be of class 'drink'
(defmethod vend ((d drink))
"Dispense one drink if available, otherwise show sold out."
;; Check if drinks are available
(if (> (drink-available d) 0)
;; If available, dispense one
(progn
;; Decrease available count
(setf (drink-available d) (1- (drink-available d)))
;; Increase consumed count
(setf (drink-consumed d) (1+ (drink-consumed d)))
;; Add to money made
(setf (drink-money-made d)
(+ (drink-money-made d) (drink-cost d)))
;; Print purchase message
(format t "~a Purchased.~%" (drink-name d)))
;; If not available, show sold out
(progn
(format t "~%")
(format t " \\\\\\\\\\\\~%")
(format t " \\ SOLD OUT \\~%")
(format t " \\\\\\\\\\\\~%")
(format t "~%"))))
(defmethod refill ((d drink))
"Restock this drink to maximum capacity."
(setf (drink-available d) (drink-maximum d)))
(defmethod profit ((d drink))
"Return the total money made, rounded to cents."
;; Round to 2 decimal places
(/ (round (* (drink-money-made d) 100)) 100.0))
(defmethod drinks-sold ((d drink))
"Return the number of drinks sold."
(drink-consumed d))
(defmethod restock-amount ((d drink))
"Return the number of drinks needed to reach full capacity."
(- (drink-maximum d) (drink-available d)))
;; ============================================================
;; MAIN PROGRAM
;; ============================================================
(defun main ()
;; Create a drink instance
(let ((coke (make-instance 'drink :name "Coke" :cost 1.75)))
;; Refill the machine
(refill coke)
(format t "After refill, ~a available: ~a~%"
(drink-name coke)
(drink-available coke))
;; Vend some drinks
(format t "~%Vending drinks:~%")
(vend coke)
(vend coke)
(vend coke)
;; Show results
(format t "~%Results:~%")
(format t "Drinks sold: ~a~%" (drinks-sold coke))
(format t "Profit: $~,2f~%" (profit coke))
(format t "Need to restock: ~a~%" (restock-amount coke))))
(main)
Output:
After refill, Coke available: 10
Vending drinks:
Coke Purchased.
Coke Purchased.
Coke Purchased.
Results:
Drinks sold: 3
Profit: $5.25
Need to restock: 3
(defmethod vend ((d drink)) ...), the (d drink) part means:
d is the parameter name (like self or this)drink is the class this method specializes ondrink (or a subclass).
While make-instance works fine, it's common to create convenience constructor functions that provide a cleaner interface:
; drink-constructor.lisp
; Demonstrating custom constructor functions in CLOS
(defclass drink ()
((name
:initarg :name
:initform "Unknown"
:accessor drink-name)
(cost
:initarg :cost
:initform 1.75
:accessor drink-cost)
(available
:initform 0
:accessor drink-available)
(consumed
:initform 0
:accessor drink-consumed)
(money-made
:initform 0.0
:accessor drink-money-made)
(maximum
:initform 10
:reader drink-maximum)))
;; ============================================================
;; CUSTOM CONSTRUCTOR FUNCTIONS
;; ============================================================
;; Constructor with just name (uses default cost of $1.75)
(defun make-drink (name)
"Create a new drink with the given name and default cost."
(make-instance 'drink :name name))
;; Constructor with name and custom cost
(defun make-drink-with-cost (name cost)
"Create a new drink with the given name and cost."
(make-instance 'drink :name name :cost cost))
;; ============================================================
;; METHOD DEFINITIONS
;; ============================================================
(defmethod vend ((d drink))
"Dispense one drink if available."
(if (> (drink-available d) 0)
(progn
(setf (drink-available d) (1- (drink-available d)))
(setf (drink-consumed d) (1+ (drink-consumed d)))
(setf (drink-money-made d)
(+ (drink-money-made d) (drink-cost d)))
(format t "~a Purchased.~%" (drink-name d)))
(format t "~a: SOLD OUT!~%" (drink-name d))))
(defmethod refill ((d drink))
"Restock to maximum capacity."
(setf (drink-available d) (drink-maximum d)))
;; ============================================================
;; MAIN PROGRAM
;; ============================================================
(defun main ()
;; Create drinks using our custom constructors
(let ((coke (make-drink "Coke")) ; Uses default $1.75
(fancy-water (make-drink-with-cost "Fiji Water" 4.50))) ; Custom price
;; Show what we created
(format t "Created drinks:~%")
(format t " ~a at $~,2f~%" (drink-name coke) (drink-cost coke))
(format t " ~a at $~,2f~%" (drink-name fancy-water) (drink-cost fancy-water))
;; Refill both
(refill coke)
(refill fancy-water)
;; Vend some
(format t "~%Vending:~%")
(vend coke)
(vend fancy-water)
(vend fancy-water)))
(main)
Output:
Created drinks:
Coke at $1.75
Fiji Water at $4.50
Vending:
Coke Purchased.
Fiji Water Purchased.
Fiji Water Purchased.
make-drink are a common pattern in Lisp. They provide a cleaner API than make-instance with keyword arguments and can include validation or setup logic.
Now let's look at a complete, working example that puts everything together. This program implements a vending machine that tracks drink sales and profits.
; vending-machine.lisp
; Complete Vending Machine Example using CLOS
;
; This program demonstrates:
; - Class definition with defclass
; - Slot definitions with various options
; - Generic functions and methods
; - Custom constructor functions
; - Object interaction
;
; Author: Student Name
; Course: Lisp Programming
;; ============================================================
;; CLASS DEFINITION
;; ============================================================
(defclass drink ()
;; The empty () means we have no superclasses
;; (other than the implicit T which all classes inherit from)
((name
;; :initarg :name means we can write (make-instance 'drink :name "Coke")
:initarg :name
;; :initform provides a default if :name isn't specified
:initform "Unknown"
;; :accessor creates both a reader (drink-name obj)
;; and a writer (setf (drink-name obj) value)
:accessor drink-name
;; :documentation describes this slot
:documentation "The name of the drink")
(cost
:initarg :cost
:initform 1.75
:accessor drink-cost
:documentation "The price of the drink in dollars")
(available
;; No :initarg - this is set internally, not at creation time
:initform 0
:accessor drink-available
:documentation "Number of drinks currently in stock")
(consumed
:initform 0
:accessor drink-consumed
:documentation "Total number of drinks sold")
(money-made
:initform 0.0
:accessor drink-money-made
:documentation "Total revenue from this drink")
(maximum
:initform 10
;; :reader creates only a reader function (no setter)
;; This makes maximum effectively read-only after creation
:reader drink-maximum
:documentation "Maximum capacity for this drink")))
;; ============================================================
;; GENERIC FUNCTION DECLARATIONS
;; ============================================================
;; Declaring generic functions is optional but good practice
;; It documents the interface and allows adding :documentation
(defgeneric vend (drink)
(:documentation "Dispense a drink if available."))
(defgeneric refill (drink)
(:documentation "Restock the drink to maximum capacity."))
(defgeneric profit (drink)
(:documentation "Return the total money made from this drink."))
(defgeneric drinks-sold (drink)
(:documentation "Return the number of drinks sold."))
(defgeneric restock-amount (drink)
(:documentation "Return the number needed to reach full capacity."))
;; ============================================================
;; CUSTOM CONSTRUCTOR FUNCTIONS
;; ============================================================
(defun make-drink (name)
"Create a new drink with the given name and default cost ($1.75).
Arguments:
name - A string naming the drink
Returns:
A new drink instance"
(make-instance 'drink :name name))
(defun make-drink-with-cost (name cost)
"Create a new drink with the given name and custom cost.
Arguments:
name - A string naming the drink
cost - The price in dollars (a number)
Returns:
A new drink instance"
(make-instance 'drink :name name :cost cost))
;; ============================================================
;; METHOD DEFINITIONS
;; ============================================================
(defmethod vend ((d drink))
"Dispense one drink if available.
If drinks are available:
- Decreases available count by 1
- Increases consumed count by 1
- Adds the drink cost to money-made
- Prints a purchase confirmation
If no drinks available:
- Prints a SOLD OUT message"
;; Check if any drinks are available
(if (> (drink-available d) 0)
;; Drinks ARE available - dispense one
(progn
;; Decrease available by 1
;; (1- x) is shorthand for (- x 1)
(setf (drink-available d) (1- (drink-available d)))
;; Increase consumed by 1
;; (1+ x) is shorthand for (+ x 1)
(setf (drink-consumed d) (1+ (drink-consumed d)))
;; Add cost to money-made
(setf (drink-money-made d)
(+ (drink-money-made d) (drink-cost d)))
;; Print purchase message
(format t "~a Purchased.~%" (drink-name d)))
;; Drinks NOT available - show sold out
(progn
(format t "~%")
(format t " \\\\\\\\\\\\\\\\\\\\~%")
(format t " \\ SOLD OUT \\~%")
(format t " \\\\\\\\\\\\\\\\\\\\~%")
(format t "~%"))))
(defmethod refill ((d drink))
"Restock this drink to maximum capacity.
Sets available to the maximum value."
(setf (drink-available d) (drink-maximum d)))
(defmethod profit ((d drink))
"Return the total money made, rounded to cents.
We round to avoid floating-point display issues."
;; Multiply by 100, round, divide by 100 to get 2 decimal places
(/ (round (* (drink-money-made d) 100)) 100.0))
(defmethod drinks-sold ((d drink))
"Return the total number of drinks sold."
(drink-consumed d))
(defmethod restock-amount ((d drink))
"Return the number of drinks needed to reach full capacity."
(- (drink-maximum d) (drink-available d)))
;; ============================================================
;; MAIN PROGRAM
;; ============================================================
(defun main ()
"Main entry point for the vending machine simulation."
;; Create drink objects using our constructor functions
;; LET* allows each binding to see the previous ones
(let* ((drink1 (make-drink "Coke"))
(drink2 (make-drink "Sprite"))
(drink3 (make-drink "Doctor Pepper"))
;; drink4 uses the custom cost constructor
(drink4 (make-drink-with-cost "V8 Splash" 4.25)))
;; Refill all drinks to maximum capacity
(refill drink1)
(refill drink2)
(refill drink3)
(refill drink4)
;; Simulate purchases
;; Purchase 8 Cokes
(vend drink1)
(vend drink1)
(vend drink1)
(vend drink1)
(vend drink1)
(vend drink1)
(vend drink1)
(vend drink1)
;; Purchase 6 Sprites
(vend drink2)
(vend drink2)
(vend drink2)
(vend drink2)
(vend drink2)
(vend drink2)
;; Purchase 2 Doctor Peppers
(vend drink3)
(vend drink3)
;; Try to purchase 11 V8 Splash (only 10 available)
(vend drink4)
(vend drink4)
(vend drink4)
(vend drink4)
(vend drink4)
(vend drink4)
(vend drink4)
(vend drink4)
(vend drink4)
(vend drink4)
(vend drink4) ; This one will fail - SOLD OUT
;; Calculate and display totals
(let ((total-sold (+ (drinks-sold drink1)
(drinks-sold drink2)
(drinks-sold drink3)
(drinks-sold drink4)))
(total-profit (+ (profit drink1)
(profit drink2)
(profit drink3)
(profit drink4))))
;; Display summary
(format t "~%")
(format t "A Total of ~a drinks were sold for a profit of $~,2f~%"
total-sold total-profit)
(format t "~%")
;; Display restocking information
(format t "~a ~a needed to restock machine~%"
(restock-amount drink1) (drink-name drink1))
(format t "~a ~a needed to restock machine~%"
(restock-amount drink2) (drink-name drink2))
(format t "~a ~a needed to restock machine~%"
(restock-amount drink3) (drink-name drink3))
(format t "~a ~a needed to restock machine~%"
(restock-amount drink4) (drink-name drink4)))))
;; Run the main function
(main)
Save the code as vending-machine.lisp and run with:
sbcl --script vending-machine.lisp
Coke Purchased.
Coke Purchased.
Coke Purchased.
Coke Purchased.
Coke Purchased.
Coke Purchased.
Coke Purchased.
Coke Purchased.
Sprite Purchased.
Sprite Purchased.
Sprite Purchased.
Sprite Purchased.
Sprite Purchased.
Sprite Purchased.
Doctor Pepper Purchased.
Doctor Pepper Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
V8 Splash Purchased.
\\\\\\\\\\
\ SOLD OUT \
\\\\\\\\\\
A Total of 26 drinks were sold for a profit of $70.50
2 Coke needed to restock machine
4 Sprite needed to restock machine
8 Doctor Pepper needed to restock machine
0 V8 Splash needed to restock machine
CLOS supports inheritance, including multiple inheritance. Here's an example of single inheritance:
; inheritance.lisp
; Demonstrating inheritance in CLOS
;; ============================================================
;; BASE CLASS
;; ============================================================
(defclass beverage ()
((name
:initarg :name
:initform "Unknown"
:accessor beverage-name)
(cost
:initarg :cost
:initform 1.00
:accessor beverage-cost)))
;; Generic function for displaying
(defgeneric display-info (beverage))
(defmethod display-info ((b beverage))
"Display basic beverage information."
(format t "Beverage: ~a at $~,2f~%"
(beverage-name b)
(beverage-cost b)))
;; ============================================================
;; DERIVED CLASS
;; ============================================================
;; CaffeinatedDrink inherits from beverage
;; Put the parent class(es) in the second argument list
(defclass caffeinated-drink (beverage)
;; Additional slot specific to caffeinated drinks
((caffeine-mg
:initarg :caffeine-mg
:initform 0
:accessor drink-caffeine)))
;; Override the display-info method for caffeinated drinks
(defmethod display-info ((d caffeinated-drink))
"Display caffeinated drink information, including caffeine content."
;; First, call the parent method using CALL-NEXT-METHOD
(call-next-method)
;; Then add our extra information
(format t " Contains ~a mg of caffeine~%" (drink-caffeine d)))
;; ============================================================
;; MAIN PROGRAM
;; ============================================================
(defun main ()
;; Create a basic beverage
(let ((water (make-instance 'beverage
:name "Spring Water"
:cost 1.50)))
(format t "=== Basic Beverage ===~%")
(display-info water))
(format t "~%")
;; Create a caffeinated drink
(let ((coffee (make-instance 'caffeinated-drink
:name "Espresso"
:cost 3.50
:caffeine-mg 150)))
(format t "=== Caffeinated Drink ===~%")
(display-info coffee))
(format t "~%")
;; Demonstrate that caffeinated-drink has inherited slots
(let ((cola (make-instance 'caffeinated-drink
:name "Cola"
:cost 1.75
:caffeine-mg 35)))
(format t "=== Another Caffeinated Drink ===~%")
(format t "Name: ~a~%" (beverage-name cola)) ; Inherited accessor
(format t "Cost: $~,2f~%" (beverage-cost cola)) ; Inherited accessor
(format t "Caffeine: ~a mg~%" (drink-caffeine cola)))) ; Own accessor
(main)
Output:
=== Basic Beverage ===
Beverage: Spring Water at $1.50
=== Caffeinated Drink ===
Beverage: Espresso at $3.50
Contains 150 mg of caffeine
=== Another Caffeinated Drink ===
Name: Cola
Cost: $1.75
Caffeine: 35 mg
call-next-method is CLOS's way of calling the parent's version of a method, similar to super.method() in Java. It follows the method resolution order to find the next applicable method.
; WRONG - will try to evaluate 'drink' as a variable
(make-instance drink :name "Coke")
; Error: The variable DRINK is unbound.
; RIGHT - quote the class name
(make-instance 'drink :name "Coke")
; WRONG - this is Java/C++ syntax
my-drink.name
my-drink->name
; WRONG - wrong order for setf
(drink-name my-drink "New Name")
; RIGHT - reading
(drink-name my-drink)
; RIGHT - writing (setf form)
(setf (drink-name my-drink) "New Name")
; WRONG - no class specialization
(defmethod vend (d)
...)
; This creates a method that accepts ANY object, not just drinks
; RIGHT - specialize on the drink class
(defmethod vend ((d drink))
...)
; Class definition
(defclass drink ()
((name :initarg :drink-name ...)))
; WRONG - using slot name instead of initarg
(make-instance 'drink :name "Coke")
; The slot won't be initialized!
; RIGHT - use the :initarg keyword
(make-instance 'drink :drink-name "Coke")
; OR better - make initarg match slot name
(defclass drink ()
((name :initarg :name ...)))
(make-instance 'drink :name "Coke")
; Slot defined with :reader only
(defclass drink ()
((maximum :initform 10 :reader drink-maximum)))
; WRONG - can't setf a reader
(setf (drink-maximum my-drink) 20)
; Error: (SETF DRINK-MAXIMUM) is undefined
; If you need to modify it, use :accessor instead of :reader
(defclass drink ()
((maximum :initform 10 :accessor drink-maximum)))
; Now this works
(setf (drink-maximum my-drink) 20)
| Task | Common Lisp (CLOS) Syntax |
|---|---|
| Define a class | (defclass name (supers) (slots)) |
| Create an instance | (make-instance 'class :slot value) |
| Define slot with accessor | (slot-name :accessor accessor-name) |
| Define slot with reader only | (slot-name :reader reader-name) |
| Set default value | :initform value |
| Set initialization keyword | :initarg :keyword-name |
| Read a slot | (accessor-name object) |
| Write a slot | (setf (accessor-name object) value) |
| Declare generic function | (defgeneric name (params)) |
| Define a method | (defmethod name ((p class)) body) |
| Call parent method | (call-next-method) |
| Check object's class | (typep object 'class-name) |
| Get object's class | (class-of object) |
defclass, containing slot definitions:accessor, :reader, or :writer to control accessmake-instance using :initarg keywordsdefmethoddefgeneric) can have multiple methods for different classescall-next-method to invoke parent class methods'drink not drink(setf (accessor obj) value) to modify slotsObjective: Modify the vending machine program to replace the V8 Splash drink with a new beverage of your choice.
Assignment:
The current program includes V8 Splash at $4.25 as the fourth drink. Your task is to:
(vend ...) calls for each drinkSpecific Changes to Make:
In the main function, change the line:
(drink4 (make-drink-with-cost "V8 Splash" 4.25))
to your new drink with a new price, for example:
(drink4 (make-drink-with-cost "Orange Juice" 3.50))
Requirements:
vend calls (you can change the number of times each is called)Testing Your Solution:
sbcl --script vending-machine.lisp🎯 Challenge:
Before running your program, calculate what the total profit should be. Does your output match your prediction?
Hint: Total profit = (# of Cokes × $1.75) + (# of Sprites × $1.75) + (# of Dr Peppers × $1.75) + (# of your drink × your price)