Overview
Method combination is a feature of object-oriented programming languages that support multiple dispatch, most notably in the Common Lisp Object System (CLOS). It defines how methods from different classes are combined and executed when a generic function is called with a set of arguments. In CLOS, the standard method combination provides a declarative way to specify the order of execution for primary, before, after, and around methods, allowing fine-grained control over behavior without explicit sequencing. Custom method combinations can also be defined for specialized needs.
1 Introduction
1.1 Definition and purpose
Method combination refers to the rules and mechanisms by which multiple methods, each applicable to a given set of argument types, are composed into a single effective method when a generic function is invoked. The purpose of method combination is to enable modular and reusable code: different classes can contribute behavior (via before, after, or around methods) without needing to modify existing primary methods, and the system automatically invokes all applicable methods in a predetermined order.
1.2 Historical context in CLOS
The Common Lisp Object System (CLOS), standardized in the late 1980s as part of Common Lisp, introduced method combination as a core concept. It was influenced by earlier Lisp-based object systems such as Flavors and New Flavors, which had mechanisms for method combination (e.g., :before and :after wrappers). CLOS generalized these ideas into a uniform, extensible framework. The standard method combination defined in the Common Lisp standard became the default, but the language also provides define-method-combination for creating custom combinations.
2 Standard method combination
The standard method combination in CLOS defines a fixed ordering of method execution, dividing methods into four categories: primary, :before, :after, and :around. Each category has a specific role and sequence.
2.1 Primary methods
Primary methods contain the main logic of the generic function. They are the only methods whose return values are normally used as the result of the generic function call (unless modified by :around methods). In standard method combination, primary methods are selected based on the most specific applicable class for each argument.
2.1.1 Role and execution order
The role of a primary method is to implement the core computation for a given set of argument types. Multiple primary methods can be applicable due to inheritance; the standard combination selects the most specific primary method according to the class precedence list of each argument. However, only one primary method is actually executed per generic function call (the most specific one). Other, less specific primary methods are not called unless a custom combination or the use of call-next-method within the primary method explicitly invokes them.
The execution order for primary methods within the standard combination is: the most specific primary method first, then optionally the next most specific if invoked via call-next-method, and so on. This is in contrast to auxiliary methods, which are all executed regardless.
2.1.2 Inheritance and precedence
Primary methods are inherited like other methods. When a generic function is called with arguments, the class of each argument determines the set of applicable primary methods. The method with the most specific class for each argument (according to the class precedence list) wins. If multiple arguments are used, the dispatch is based on the combination of all argument types; the most specific method is the one that matches all argument types with the highest combined specificity.
2.2 Auxiliary methods
Auxiliary methods are used to add behavior around primary methods without modifying them. The standard method combination defines three qualifiers: :before, :after, and :around.
2.2.1 :before methods
A :before method is qualified with :before and runs _before_ the primary method. It is invoked for every applicable :before method, from the least specific class to the most specific class. :before methods typically perform setup or logging. Their return values are ignored.
2.2.2 :after methods
An :after method runs _after_ the primary method. All applicable :after methods are invoked, but in reverse order of specificity: the most specific :after method runs first, then the next most specific, and so on. :after methods are often used for cleanup or post-processing. Their return values are also ignored.
2.2.3 :around methods
:around methods provide a wrapper around the entire execution of other methods. They are called with the same arguments as the generic function and can choose to call the rest of the effective method (which includes the :before, primary, and :after methods) using call-next-method. If an :around method does not call call-next-method, the inner methods are not executed.
2.2.3.1 Wrapping behavior
The wrapping behavior of :around methods allows them to modify the arguments passed to inner methods, alter the return value, or conditionally skip the inner method entirely. Multiple :around methods are ordered from most specific to least specific; each can wrap the next. The outermost :around method acts as the entry point.
2.3 Execution order
The standard method combination executes methods in a strict four-phase order, as detailed below.
2.3.1 Around methods first
All applicable :around methods are considered first. They are invoked starting with the most specific (closest to the given object) and proceeding outward. Only the most specific :around method runs immediately; it may call call-next-method to invoke the next :around or, if none, the inner :before, primary, and :after sequence.
2.3.2 Before methods second
After the outermost :around method (if any) calls call-next-method, and before the primary, all applicable :before methods are executed. They run from least specific to most specific (i.e., from the top of the class hierarchy downward to the most derived).
2.3.3 Primary methods third
Next, the most specific primary method is executed. If that method calls call-next-method, the next most specific primary method is invoked, and so on, until no more primary methods are available.
2.3.4 After methods last
Finally, after the primary method(s) return, all applicable :after methods are executed. They run from most specific to least specific (i.e., in reverse order of :before methods). After the last :after method returns, control returns to the :around methods (if any) and ultimately to the caller.
3 Custom method combinations
CLOS allows programmers to define custom method combinations beyond the standard one. This is done using the macro define-method-combination.
3.1 Defining custom combinations
The define-method-combination macro takes a name and a series of clauses that specify how methods of different qualifiers should be combined.
3.1.1 Using define-method-combination
A typical call to define-method-combination defines the type of combination (e.g., :progn, :and, :or, :list, etc.) and specifies the order in which methods of various qualifiers are invoked. The programmer can also specify a default qualifier for methods that have no explicit qualifier (usually primary methods).
3.1.2 Long form vs short form
The macro has two forms: a short form and a long form. The short form is used for simple combinations (like :progn and :and). It only requires specifying the type and optional default method group. The long form provides full control, allowing the programmer to define how methods of different types are grouped, ordered, and combined using Lisp forms. For example, a custom combination might execute all primary methods and accumulate their results into a list.
3.2 Common custom patterns
Several common patterns are provided as built-in combination types in CLOS, though they can also be user-defined.
3.2.1 :progn, :and, :or
:progn– Executes all applicable primary methods in order (most specific first) and returns the value of the last method.:and– Executes primary methods in order; if any returnsnil, stops and returnsnil. Otherwise returns the value of the last method.:or– Executes primary methods in order; if any returns a non-nilvalue, stops and returns that value. Otherwise returnsnil.
3.2.2 :list, :append, :nconc
:list– Collects the return values of all applicable primary methods into a list, in order.:append– Assumes each primary method returns a list; appends all such lists into a single list (preserving order).:nconc– Similar to:appendbut uses destructive concatenation (for performance).
4 Method combination in other languages
While method combination is most deeply integrated in CLOS, other languages have adopted related concepts for combining methods or functions in inheritance hierarchies.
4.1 Python's super() and MRO
Python uses a method resolution order (MRO) for single and multiple inheritance, governed by the C3 linearization algorithm. The built-in super() function allows a method to delegate to the next class in the MRO. This can be seen as a limited form of method combination: super() effectively calls the next method in the chain, similar to call-next-method in CLOS. However, Python does not have separate :before, :after, or :around qualifiers; instead, programmers can simulate them by explicitly calling super() before or after their own code.
4.2 Multi-methods in Julia
Julia employs multiple dispatch as its core paradigm. It allows defining methods for a generic function based on the types of all arguments. Julia does not have a built-in method combination system like CLOS; however, it can be emulated using function composition or macros. The language does provide the invoke function for calling a specific method directly, which is somewhat analogous to call-next-method. Libraries such as MethodCombination.jl bring CLOS-like capabilities to Julia.
4.3 C++ virtual inheritance
C++ virtual inheritance is used to resolve diamond inheritance problems, but it does not support multiple dispatch or method combination in the CLOS sense. C++ uses virtual functions that are resolved based on the dynamic type of the object (single dispatch). Programmers can achieve before/after behavior by manually calling base class methods in derived class overrides, but there is no declarative method combination mechanism.
5 Examples and use cases
5.1 Logging with :before and :after
In CLOS, one can add logging behavior to a generic function without modifying its primary methods:
(defgeneric process (object)
(:method-combination standard))
(defmethod process ((obj some-class))
;; primary method
(format t "Processing ~a~%" obj))
(defmethod process :before ((obj t))
(format t "Entering process with ~a~%" obj))
(defmethod process :after ((obj t))
(format t "Exiting process~%"))
Every call to process automatically prints the before and after messages.
5.2 Validation with :around
:around methods can validate arguments before allowing the primary method to run:
(defmethod process :around ((obj integer))
(if (>= obj 0)
(call-next-method)
(error "Negative numbers not allowed")))
If the argument is negative, the primary method is skipped and an error is signaled.
5.3 Conditional execution with :or
With a custom :or combination, one can define a generic function that tries several primary methods in order until one returns a non-nil value:
(define-method-combination or-methods :or)
(defgeneric find-handler (error-condition)
(:method-combination or-methods))
(defmethod find-handler ((err division-by-zero))
(format nil "Division by zero handled"))
(defmethod find-handler ((err generic-error))
(format nil "Generic handler"))
If the first method returns nil (or doesn’t apply), the second is tried.
6 Comparison with method overriding
6.1 Single dispatch vs multiple dispatch
Single dispatch languages (e.g., Java, C++) resolve methods based solely on the receiver's object type. Method overriding replaces the parent method entirely. In multiple dispatch (as in CLOS, Julia), the method resolution depends on all arguments, allowing more flexible combinations. Method combination is typically found only in multiple dispatch systems because it allows multiple applicable methods to coexist and share control.
6.2 Static vs dynamic method resolution
Static (compile-time) resolution, as in C++ virtual functions, uses a fixed vtable and does not support adding behavior around existing methods without recompilation. Dynamic resolution, as in CLOS, can combine methods at runtime, enabling libraries to augment existing generic functions without modifying source code. Method combination thus provides a powerful form of aspect-oriented programming, where cross-cutting concerns (logging, security, caching) can be added declaratively.
7 Advanced topics
7.1 Method qualifiers and lambda lists
Method qualifiers in CLOS are symbols (like :before, :after, :around) that appear before the argument lambda list in a method definition. Lambda lists themselves can include required, optional, keyword, and &rest parameters. The combination system ensures that all methods in a generic function have compatible lambda lists; otherwise an error is signaled. Custom combinations can define their own qualifiers.
7.2 Combination with generic function discriminations
When a generic function is called, the dispatch mechanism (discrimination) selects the set of applicable methods. This set is then processed by the method combination to produce an effective method. The discrimination can be optimized using dispatch tables, memoization, or inline caching. Custom combinations may require additional discrimination logic if they reorder or filter methods based on runtime conditions.
7.3 Performance considerations
Method combination introduces overhead because the system must compute the effective method at each call (or cache it). CLOS implementations often cache the effective method for a given set of argument types to avoid repeated combination. Custom combinations, especially complex ones, can increase this cost. However, for most applications, the overhead is negligible compared to the flexibility gained. Languages like Julia that do not have method combination rely on separate dispatch for each type signature, which can be faster for simple cases but lacks the declarative composition of CLOS.