Skip to content

LISP - Class Precedence

Lisp - Understanding Class Precedence in CLOS

Section titled “Lisp - Understanding Class Precedence in CLOS”

The Common Lisp Object System (CLOS) features a powerful multiple inheritance system. When a class inherits from multiple parent classes (superclasses), a potential ambiguity arises: if multiple superclasses provide a method with the same name, which one should be used? Class Precedence resolves this ambiguity by defining a single, linear order of inheritance for every class.

Determining the Class Precedence List (CPL)

Section titled “Determining the Class Precedence List (CPL)”

CLOS computes a Class Precedence List (CPL) for each class using a sophisticated algorithm (C3 Method Resolution Order). The CPL determines the complete inheritance hierarchy. The rules can be simplified as follows:

  • A class always has higher precedence than its superclasses.
  • The order of direct superclasses listed in the defclass form is preserved.
  • The final list is a consistent, deterministic linearization of the entire class hierarchy.
  • The built-in classes standard-object and t are always at the end of the list, providing a common root.

You can inspect the CPL of any class using the function (class-precedence-list (find-class 'class-name)). For a more readable output, we can map class-name over the result.

Let’s see how the order of superclasses in defclass affects the CPL.

;;; main.lisp - Demonstrating Class Precedence
(defclass a () ())
(defclass b () ())
;; Class C inherits from A, then B
(defclass c (a b) ())
;; Class D inherits from B, then A
(defclass d (b a) ())
(defun show-cpl (class-symbol)
"Prints the class precedence list for a given class symbol."
(format t "CPL for ~a: ~a~%"
class-symbol
(mapcar #'class-name (ccl:class-precedence-list (find-class class-symbol)))))
(show-cpl 'a)
(show-cpl 'b)
(show-cpl 'c)
(show-cpl 'd)

Note: The function class-precedence-list is not exported by the ANSI standard. Most implementations provide it in a package like ccl, sb-mop, or clos. We use ccl:class-precedence-list here as an example.

When you execute the code, the output clearly shows the linearized order:

CPL for A: (A STANDARD-OBJECT T)
CPL for B: (B STANDARD-OBJECT T)
CPL for C: (C A B STANDARD-OBJECT T)
CPL for D: (D B A STANDARD-OBJECT T)
  • For c, since it’s defined as (defclass c (a b) ()), its CPL is (C A B ...). Class A has precedence over B.
  • For d, defined as (defclass d (b a) ()), its CPL is (D B A ...). Class B has precedence over A.

The primary purpose of the CPL is to determine which method to execute when a generic function is called. CLOS finds all applicable methods for the given arguments and executes the most specific one, as determined by the CPL.

;; Define a generic function
(defgeneric identify (obj))
;; Define methods for classes A and B
(defmethod identify ((obj a))
"Method specialized for class A")
(defmethod identify ((obj b))
"Method specialized for class B")
;; Create instances of C and D
(let ((instance-c (make-instance 'c))
(instance-d (make-instance 'd)))
(format t "Calling identify on an instance of C: ~s~%" (identify instance-c))
(format t "Calling identify on an instance of D: ~s~%" (identify instance-d)))
Calling identify on an instance of C: "Method specialized for class A"
Calling identify on an instance of D: "Method specialized for class B"
  • For instance-c, both identify methods (for a and b) are applicable. Since A comes before B in C’s CPL, the method for A is chosen as the most specific.
  • For instance-d, the situation is reversed. B comes before A in D’s CPL, so the method for B is chosen.
  • Predictability: Class precedence provides a predictable and powerful mechanism for resolving method conflicts in complex inheritance hierarchies.
  • Control: As a developer, you control the precedence by ordering the superclasses in defclass.
  • Debugging: When unsure about method behavior, inspecting the class-precedence-list is the first step in debugging.
  • Advanced Methods: This is the foundation for more advanced features like method combination (e.g., :before, :after, and :around methods), which allow multiple methods to contribute to a single generic function call.