Skip to content

LISP - :around Method Combination

The Common Lisp Object System (CLOS) provides a powerful feature called method combination. The standard method combination uses primary methods along with :before, :after, and :around auxiliary methods. Of these, :around is the most powerful, as it allows you to write code that runs before and after the primary method, effectively creating a wrapper around it.

Think of an :around method like middleware in a web framework or a decorator in Python. It can inspect arguments, set up a special context, run the core logic, and then process the result or perform cleanup.

An :around method is defined by adding the :around qualifier to defmethod. The key to its power is the call-next-method function.

;; 1. Define a generic function.
(defgeneric process-data (data))
;; 2. Define an :around method for this generic function.
(defmethod process-data :around ((data t))
(format t "[Wrapper] Before processing data: ~a~%" data)
;; 3. Use `call-next-method` to invoke the next most specific method.
(let ((result (call-next-method)))
(format t "[Wrapper] After processing. Result was: ~a~%" result)
;; 4. The :around method can return the original result or a modified one.
result))

The call-next-method function transfers control to the next applicable method (which could be another :around method or the primary method). Without call-next-method, the primary method will not be executed.

When process-data is called, the flow is:

  1. The :around method is invoked first.
  2. Code before call-next-method runs.
  3. call-next-method invokes the applicable primary method (or other auxiliary methods).
  4. The primary method runs and returns a value.
  5. Control returns to the :around method, and the returned value is bound to result.
  6. Code after call-next-method runs.
  7. The :around method returns its final value.

Let’s add primary methods specialized for different data types.

;;; --- Generic function and :around method (from above) ---
(defgeneric process-data (data))
(defmethod process-data :around ((data t))
(format t "[Wrapper] Before processing data: ~a~%" data)
(let ((result (call-next-method)))
(format t "[Wrapper] After processing. Result was: ~a~%" result)
result))
;;; --- Primary Methods ---
;; Define a primary method specialized for INTEGERs.
(defmethod process-data ((data integer))
(format t " (Primary) Processing an integer...~%")
(* data 2))
;; Define a primary method specialized for STRINGs.
(defmethod process-data ((data string))
(format t " (Primary) Processing a string...~%")
(string-upcase data))
;;; --- Invocation ---
(format t "--- Calling with an integer ---~%")
(process-data 10)
(format t "~%--- Calling with a string ---~%")
(process-data "hello world")
--- Calling with an integer ---
[Wrapper] Before processing data: 10
(Primary) Processing an integer...
[Wrapper] After processing. Result was: 20
--- Calling with a string ---
[Wrapper] Before processing data: hello world
(Primary) Processing a string...
[Wrapper] After processing. Result was: "HELLO WORLD"
  • Resource Management: Ensuring resources like files or network connections are closed (e.g., unwind-protect inside an :around method).
  • Caching/Memoization: Checking if a result is already cached before calling call-next-method.
  • Logging and Tracing: Printing arguments on entry and results on exit.
  • Argument Validation: Validating arguments before passing them to the primary method.
  • Conditional Execution: Deciding whether to call the primary method at all based on some condition.
  • Forgetting call-next-method: This is the most common error. If you omit this call, the primary method is never run, and your generic function will likely not do what you expect.
  • Incorrectly Modifying Arguments: If you want to change the arguments for the next method, you must pass them to call-next-method: (call-next-method new-arg1 new-arg2).