CLOS : Common Lisp Object System

Defining Classes

(defclass animal ()
  ((name  :initarg :name  :accessor animal-name :initform "unknown")
   (sound :initarg :sound :accessor animal-sound)))

Creating Instances

(defvar *annie* (make-instance 'animal :name "Annie" :sound "Grunt"))
(animal-name *annie*)   ; => "Annie"
(animal-sound *annie*)  ; => "Grunt"

Inheritance

(defclass dog (animal)
  ((breed :initarg :breed :accessor dog-breed)))

(defvar *donnie* (make-instance 'dog :name "Donnie" :breed "Labrador"))
(animal-name *donnie*)  ; => "Donnie"  (inherited slot)
(dog-breed *donnie*)    ; => "Labrador"

Generic Functions & Methods

(defgeneric speak (animal)
  (:documentation "Make the animal speak"))

(defmethod speak ((a animal))
  (format nil "~a says ~a!" (animal-name a) (animal-sound a)))

(defmethod speak ((d dog))
  (format nil "The dog ~a barks loudly!" (animal-name d)))

(speak *annie*)   ; => "Rex says Woof!"
(speak *donnie*)   ; => "The dog Donnie barks loudly!"

Call-Next-Method

(defmethod speak ((d dog))
  (concatenate 'string "[Dog specific] " (call-next-method)))

(speak *donnie*)  ; => "[Dog specific] The dog Donnie barks loudly!"

Method Qualifiers (Before/After/Around)

(defmethod speak :before ((a animal))
  (format t "--- About to speak ---~%"))

(defmethod speak :after ((a animal))
  (format t "--- Done speaking ---~%"))

(defmethod speak :around ((a animal))
  (format t "~%")
  (call-next-method)   ; runs before/primary/after chain
  (format t "~%"))

Slot Access

(slot-value *annie* 'name)          ; => "Annie"
(setf (animal-name *annie*) "Max")  ; using accessor
(setf (slot-value *annie* 'name) "Max")  ; direct slot access

Checking Types

(typep *annie* 'dog)     ; => T
(typep *annie* 'animal)  ; => T (inheritance)
(class-of *annie*)       ; => #<STANDARD-CLASS DOG>

Multiple Inheritance

(defclass pet ()
  ((owner :initarg :owner :accessor pet-owner)))

(defclass pet-dog (dog pet) ())  ; inherits from both

(defvar *buddy* (make-instance 'pet-dog
                  :name "Buddy"
                  :owner "Alice"
                  :breed "Poodle"))
(pet-owner *buddy*)   ; => "Alice"
(dog-breed *buddy*)   ; => "Poodle"

Method Resolution Order (MRO) follows left-to-right depth-first, so pet-dog checks: pet-dog → dog → animal → pet.

Multiple Dispatch

CLOS dispatches on all arguments, not just the first (unlike most OO languages).

Basic Example

(defgeneric interact (a b)
  (:documentation "Interaction between two entities"))

(defclass cat (animal) ())
(defclass dog (animal) ())

(defmethod interact ((d dog) (c cat))
  (format t "~a chases ~a!~%" (animal-name d) (animal-name c)))

(defmethod interact ((c cat) (d dog))
  (format t "~a hisses at ~a!~%" (animal-name c) (animal-name d)))

(defmethod interact ((d1 dog) (d2 dog))
  (format t "~a and ~a play together!~%" (animal-name d1) (animal-name d2)))

(defvar *annie* (make-instance 'dog :name "Annie"))
(defvar *fido* (make-instance 'dog :name "Fido"))
(defvar *whiskers* (make-instance 'cat :name "Whiskers"))

(interact *annie* *whiskers*)  ; => "Annie chases Whiskers!"
(interact *whiskers* *annie*)  ; => "Whiskers hisses at Annie!"
(interact *annie* *fido*)      ; => "Annie and Fido play together!"

Serialization Example

A more practical use case — dispatching on both object type and format:

(defclass json-format () ())
(defclass xml-format  () ())

(defclass person ()
  ((name :initarg :name :accessor person-name)
   (age  :initarg :age  :accessor person-age)))

(defclass product ()
  ((id    :initarg :id    :accessor product-id)
   (price :initarg :price :accessor product-price)))

(defgeneric serialize (object format)
  (:documentation "Serialize OBJECT to FORMAT"))

;; Person → JSON
(defmethod serialize ((p person) (f json-format))
  (format nil "{\"name\": \"~a\", \"age\": ~a}"
          (person-name p) (person-age p)))

;; Person → XML
(defmethod serialize ((p person) (f xml-format))
  (format nil "<person><name>~a</name><age>~a</age></person>"
          (person-name p) (person-age p)))

;; Product → JSON
(defmethod serialize ((p product) (f json-format))
  (format nil "{\"id\": ~a, \"price\": ~a}"
          (product-id p) (product-price p)))

;; Fallback
(defmethod serialize (object format)
  (error "No serializer for ~a to ~a" (class-of object) (class-of format)))

(defvar *json* (make-instance 'json-format))
(defvar *xml*  (make-instance 'xml-format))
(defvar *bob*  (make-instance 'person :name "Bob" :age 30))
(defvar *prod* (make-instance 'product :id 42 :price 9.99))

(serialize *bob*  *json*)
;; => "{\"name\": \"Bob\", \"age\": 30}"

(serialize *bob*  *xml*)
;; => "<person><name>Bob</name><age>30</age></person>"

(serialize *prod* *json*)
;; => "{\"id\": 42, \"price\": 9.99}"

(serialize *prod* *xml*)
;; => Error: No serializer for PRODUCT to XML-FORMAT

Key Points

  • Dispatch happens on the runtime type of all specialized arguments
  • More specific methods take priority over less specific ones
  • A method with no specializers acts as a fallback
  • This replaces the need for instanceof chains or visitor patterns found in other languages

Author: Breanndán Ó Nualláin <o@uva.nl>

Date: 2026-04-22 Wed 15:37