Skip to content

LISP - :after Method Combination

The Common Lisp Object System (CLOS) offers a powerful feature called method combination, which controls how different methods for a generic function are combined and executed. The standard method combination uses qualifiers like :after to define auxiliary methods that run in addition to the primary method. Think of them as ‘hooks’ or ‘middleware’ that let you reliably execute code after the main logic.

The :after method qualifier is particularly useful for:

  • Resource Cleanup: Ensuring files or network connections are closed after use.
  • Logging and Auditing: Recording that an action was completed.
  • State Updates: Refreshing a cache or updating a UI after data has changed.

You define an :after method using defmethod with the :after qualifier placed between the generic function name and the parameter list. The return values of :after methods are always ignored.

;; 1. Define a generic function.
(defgeneric process-data (data))
;; 2. Define an :after method.
;; This method will run *after* any applicable primary method for `process-data`.
(defmethod process-data :after ((data t))
(format t "[LOG] Finished processing data: ~a~%" data))
;; 3. Define another, more specific :after method.
(defmethod process-data :after ((data integer))
(format t "[CLEANUP] Integer-specific cleanup complete.~%"))

When a generic function is called, CLOS runs the applicable methods in a specific order:

  1. The most specific :around method (if any).
  2. All applicable :before methods, from most-specific to least-specific.
  3. The most-specific primary method only.
  4. All applicable :after methods, from least-specific to most-specific.

Let’s see a complete example demonstrating the call order and use case.

;; 1. Generic function definition
(defgeneric process-data (data))
;; 2. Auxiliary :after methods
;; This is the LEAST specific :after method (applies to all types `t`)
(defmethod process-data :after ((data t))
(format t "--> :after method for T (least specific) called.~%"))
;; This is a MORE specific :after method (applies only to integers)
(defmethod process-data :after ((data integer))
(format t "--> :after method for INTEGER (most specific) called.~%"))
;; 3. Primary methods (the main logic)
;; Primary method for integers
(defmethod process-data ((data integer))
(format t "Running PRIMARY method for INTEGER.~%")
(* data 2)) ; The return value of the primary method is the return value of the whole call.
;; Primary method for strings
(defmethod process-data ((data string))
(format t "Running PRIMARY method for STRING.~%")
(concatenate 'string data "-processed"))
;; --- Function Calls ---
(format t "--- Calling (process-data 10) ---~%")
(process-data 10)
(format t "~%--- Calling (process-data \"test\") ---~%")
(process-data "test")

When you execute the code, you’ll see the precise order of execution:

--- Calling (process-data 10) ---
Running PRIMARY method for INTEGER.
--> :after method for T (least specific) called.
--> :after method for INTEGER (most specific) called.
--- Calling (process-data "test") ---
Running PRIMARY method for STRING.
--> :after method for T (least specific) called.
  • When (process-data 10) is called: The primary method for integer runs first. Then, the :after methods are executed, starting with the least specific (t) and ending with the most specific (integer).
  • When (process-data "test") is called: The primary method for string runs. Afterward, only the :after method for t is applicable, so it runs.
  • Return Values: Notice that the return values from the :after methods are discarded. The final return value of the generic function call is always the value returned by the primary method.

If you are unsure which methods apply to a generic function, you can use describe at the REPL:

;; In your REPL, after defining the methods:
CL-USER> (describe 'process-data)
;; The output will list all defined methods and their qualifiers.