Skip to content

LISP - Method Combinations

The Common Lisp Object System (CLOS) is one of the most powerful object systems available. A key feature that sets it apart is method combination, which defines how CLOS combines the behavior of multiple applicable methods for a single generic function call. This allows for incredibly flexible and modular program design.

First, a quick refresher:

  • A generic function defines an interface for an operation (e.g., draw-shape).
  • A method provides a specific implementation of that generic function for certain argument types (e.g., a method for (draw-shape ((s circle))) and another for (draw-shape ((s square)))).

Method combination answers the question: What happens when inheritance means multiple methods could apply to a single call?

This is the default and most common type of method combination. It orchestrates four different kinds of methods, which run in a specific, layered order. We can use the analogy of preparing a meal to understand it:

  • 1. :around Methods: The Head Chef. Has ultimate control. It can run its own code, decide whether to proceed with the rest of the meal by using (call-next-method), and can even alter the final result. The most specific :around method runs first.
  • 2. :before Methods: The Prep Work. These all run before the main action, in most-specific-first order (e.g., wash vegetables, then chop vegetables). They do not provide a return value.
  • 3. Primary Methods: The Main Course. Only the single most specific primary method runs. It defines the core behavior and provides the return value for the meal (unless an :around method overrides it).
  • 4. :after Methods: The Cleanup. These all run after the main action, in most-specific-last order (e.g., clean the counter, then wash the dishes). They do not provide a return value.

Let’s define a class hierarchy and a generic function with all four method types.

;; main.lisp
(defclass vehicle () ())
(defclass car (vehicle) ())
;; 1. Define the generic function
(defgeneric perform-maintenance (v))
;; 2. Define the methods
;; :around method - has overall control
(defmethod perform-maintenance :around ((v vehicle))
(format t "[AROUND] Starting maintenance check for ~a.~%" (class-name (class-of v)))
(let ((report (call-next-method)))
(format t "[AROUND] Maintenance complete. Report: ~a~%" report)
(cons 'completed report)))
;; :before methods - run most-specific-first
(defmethod perform-maintenance :before ((v vehicle))
(format t " [BEFORE] Checking tire pressure.~%"))
(defmethod perform-maintenance :before ((v car))
(format t " [BEFORE] Checking engine oil level.~%"))
;; Primary methods - only the most specific one runs
(defmethod perform-maintenance ((v vehicle))
(format t " [PRIMARY] Washing the vehicle.~%")
'(washed))
(defmethod perform-maintenance ((v car))
(format t " [PRIMARY] Rotating tires.~%")
'(tires-rotated))
;; :after methods - run most-specific-last
(defmethod perform-maintenance :after ((v vehicle))
(format t " [AFTER] Logging maintenance in vehicle logbook.~%"))
(defmethod perform-maintenance :after ((v car))
(format t " [AFTER] Resetting 'Service Needed' light.~%"))
;; 3. Call the generic function
(perform-maintenance (make-instance 'car))
; Execution Flow for a 'car' instance:
; 1. The :around method starts.
; 2. It calls next, triggering :before methods (car, then vehicle).
; 3. The most specific primary method (car) runs and returns '(tires-rotated)'.
; 4. :after methods run (vehicle, then car).
; 5. Control returns to the :around method, which wraps the result.
[AROUND] Starting maintenance check for CAR.
[BEFORE] Checking engine oil level.
[BEFORE] Checking tire pressure.
[PRIMARY] Rotating tires.
[AFTER] Logging maintenance in vehicle logbook.
[AFTER] Resetting 'Service Needed' light.
[AROUND] Maintenance complete. Report: (TIRES-ROTATED)
; Return value -> (COMPLETED TIRES-ROTATED)

CLOS provides other ways to combine methods, where all applicable primary methods contribute to the result. You specify this in the defgeneric form.

  • +: The results of all applicable methods are added together.
  • append: The list results of all methods are appended into a single list.
  • and: If any method returns nil, the combination stops and returns nil. Otherwise, returns the value from the last method.
  • list: Gathers the results of all methods into a list.
(defclass item () ())
(defclass taxable-item (item) ())
(defclass shipping-item (item) ())
(defgeneric get-total-cost (i)
(:method-combination + :most-specific-first))
(defmethod get-total-cost + ((i item)) 100) ; Base price
(defmethod get-total-cost + ((i taxable-item)) 8) ; Tax
(defmethod get-total-cost + ((i shipping-item)) 15) ; Shipping
;; An item that is both taxable and shippable
(defclass book (taxable-item shipping-item) ())
(get-total-cost (make-instance 'book))
;=> 123 (100 + 8 + 15)
  • Separation of Concerns: Keep core logic in primary methods, while handling peripheral tasks like logging, security, or caching in :before, :after, or :around methods.
  • Framework Extensibility: Allows users of a library to hook into and customize behavior without modifying the original source code.
  • Modular Design: Build complex behaviors from small, reusable, and independent methods.