Guard methods on CLOS generic functions

There are times when one wants to be able to guard a method’s execution. A typical case is for callbacks, where we only want the callback to run under certain circumstances – but it’s easier to write the callbacks themselves as though they’ll always be called.

Object-oriented programs typically use a pattern for this: they split the function into two methods, one for the guard and one for the action being guarded. A sub-class can then override the guard independently of the action, and some sub-classes may override both guard and action.

This splitting seems a little awkward, though, and there are times when I’d prefer to have everything (guard and action) defined as part of the one method. Fortunately there’s a Lisp-ier solution involving defining a new method combination to get exactly this behaviour.

Standard method combination

CLOS, unlike most languages, allows a programmer to control how methods are combined in terms of overriding. The “standard” combination allows for :before, :after, and :around methods as well as undecorated “primary” methods.

When a generic function is called, the list of applicable primary methods is determined based on the types of arguments. most specific method first1.

The same process is performed for all applicable :before methods, and then again for applicable :after methods, and then again for :around methods. The :after and :around are always ordered most specific first, while the :before methods are always ordered least-specific first.

Once these lists have been constructed, the “effective” method that results is called. If there are :around methods, they are called in order. An :around method may, as part of its body, call call-next-method to invoke the next-most-specific :around method – or may not.

If a call to call-next-method has no more :around methods to call – or of there were no :around methods defined – all the :before methods are run and their return values discarded. Then the primary methods are run in the same manner as :around methods, with any calls to call-next-method calling the next primary method. After the primary methods have returned, all the :after methods are run and their return values discarded. The result of the method call is the result returned from the primary methods. The process is roughly like:

     (if arounds
         (call-method (car arounds) (cdr arounds)))

     (prog1
         (progn
           (call-methods befores)
           (call-method (car primaries) (cdr primaries)))
       (call-methods afters))

In this code, call-method calls its first method argument, and any call to call-next-method calls the next method in the list. call-methods calls all the methods in a list of methods, discarding their return values.

Contrast that with Java or Python, where methods on more-specific classes override those on less-specific, and have the option to call up to the superclass method. Essentially this makes all methods similar to :around, and there’s no real equivalent of :before and :after.

Other method combinations

The above is referred to as standard method combination, implying the existence of non-standard combination. CLOS lets the programmer define new combinations, and indeed defines a few itself. For our purposes the most important alternative method combination is and, which runs all primary methods within an and form treating all methods as predicates. There are only primary methods allowed.

Guards as method combination

For our use case, we want to be able to return values from primary methods, and allow :around, :before and :after methods. However, we also want to have some methods act as predicates that guard the execution of the effective method thus formed. We want to be able to add guard methods that are always run first, regardless of their specificity, and then run the effective method only if all the guards are satisfied. The net result is that all parts of the generic function are provided as methods on it, but some can now be boolean guards that act as gatekeepers on the rest of the methods. Naturally we want the guards to be selected for specificity alongside the other methods, letting the CLOS machinery pick all the functionality that’s appropriate to a particular method call.

Why this isn’t just :around

It might sound like we can get this behaviour using :around methods that perform guarding. But we can’t – quite.

Suppose we define a primary method:

     (defmethod example ((v integer))
       (* v 2))

We can write a guard quite happily as an :around method:

     (defmethod example :around ((v number))
       (when (> v 10)
         (call-next-method)))

This method will only allow the method to proceed when the condition holds, otherwise it returns nil.

     (list (example 26) (example 2))

(52 NIL)

So far so good.

However, the problem is that CLOS orders the :around methods most specific first. Suppose we have another :around method specialised against a more specific type:

     (defmethod example :around ((v integer))
       (if (= v 5)
           (+ v 1)
           (call-next-method)))

When this method is called with an integer this method gets run before the previous guard:

     (example 5)

6

and we get a non-nil result, despite the guard method indicating that we shouldn’t. If we provide an argument that doesn’t trigger the first :around method, then we can get caught by the guard:

     (example 6)

NIL

This is of course perfectly sensible behaviour in many cases. However, it does mean that the “guards” we’re supplying are executed as part of the effective method rather than before it, and therefore can’t guarantee that the method is properly guarded by all the guards, regardless of their specialisation. Another way of looking at this is that a later, more specialised, “guard” can override one set by an earlier, less specialised, method, which again may not be what’s desired.

A guarded method combination

Fortunately we can get the behaviour we want by defining a new method combination, guarded. A guarded generic function accepts five method qualifiers:

  • undecorated primary methods;
  • :before and :after methods that run before and after the primary methods;
  • :around methods that run around the :before-primary-:after combination; and
  • :if methods that act as guards, running before any :around methods to determine whether any of the “functional” methods are run or not

We first need a helper function2 to construct the code to run the chain of :before and :after methods while discarding their return values.

     (defun call-methods (methods)
       "Return `call-method' forms for all METHODS."
       (mapcar #'(lambda (m)
                   `(call-method ,m))
               methods))

We can then use the macro define-method-combination to define our new method combination.

     (define-method-combination guarded (&optional (order :most-specific-first))
       ((arounds (:around))
        (ifs (:if))
        (befores (:before))
        (primaries () :order order :required t)
        (afters (:after)))

       (let* ((before-form (call-methods befores))
              (after-form (call-methods afters))
              (primary-form `(call-method ,(car primaries) ,(cdr primaries)))
              (core-form (if (or befores afters (cdr primaries))
                             `(prog1
                                  (progn
                                    ,@before-form
                                    ,primary-form)
                                ,@after-form)

                             `(call-method ,(car primaries))))
              (around-form (if arounds
                               `(call-method ,(car arounds)
                                             (,@(cdr arounds)
                                              (make-method ,core-form)))

                               core-form)))

         (if ifs
             `(if (and ,@(call-methods ifs))
                  ,around-form)

             around-form)))

The macro is described in detail in the hyperspec, but its behaviour is quite simple. The list of forms (arounds and so on) define variables that extract the methods that have the given decorations – so arounds gets a list of :around methods, primaries gets the undecorated (primary) methods, and so on. In particular, ifs gets any methods decorated with :if: these are the guards.

The body of the macro constructs the code needed to build the methods’ behaviours. The let* defines the code for the different parts. core-form is slightly optimised in the case when there is only one primary method; otherwise it runs the :before methods and then the primary method, captures the result of the latter, then runs the :after methods, and then returns its result. (This is the first time I’ve ever used prog1 for real: now I know why it exists.) If there are :around methods, around-form wraps up a list consisting of the :around methods and a method constructed from core-form, letting it be run as the result of the final call-next-method call.

The body of the let* wraps-up around-form within an if whose condition is the conjunction of all the :if methods. Only if all these methods return true (well, not nil in the usual Lisp style) will the code of around-form be executed. Again the code is optimised for the case where there are no guards, in which case we just get around-form.

Notice that define-method-combination returns code, like all macros: it doesn’t execute the methods itself. This is a hint as to what happens off-stage: CLOS uses the method combination at compile time to construct effective methods which can then be cached to minimise the performance hit from all the flexibility provided by method combination.

Now we can re-do our example from above:

     ;; a generic function defined to use our new method combination
     (defgeneric guarded-example (v)
       (:method-combination guarded))

     ;; the functionality, split into two methods
     (defmethod guarded-example ((v integer))
       (* v 2))

     (defmethod guarded-example :around ((v integer))
       (if (= v 5)
           (+ v 1)
           (call-next-method)))

     ;; this guard used to be :around and is now :if
     (defmethod guarded-example :if ((v number))
       (> v 10))

     (list (guarded-example 26) (guarded-example 2) (guarded-example 5))

(52 NIL NIL)

The guard now stops execution of the effective method if its condition isn’t met – and if it is met, passes control through to the complete method stack. This happens regardless of where the guard is specialised in terms of the class hierarchy: the guards run before any “functional” code. (That :before and :after methods work too, and multiple guards, and that the combination works when applied to class hierarchies, are left as exercises to the reader.)

Critique

You may object to this solution on the grounds that it introduces a weird asymmetry into methods: some as functional and some as guards, with different return types. Maybe you prefer to keep guards in separate methods using the usual object-oriented pattern. That’s entirely reasonable. But I think there are sufficient cases where this kind of guarding makes sense to have it as a pattern, especially as it has no effect unless explicitly selected for a generic function.

I have to say I’m amazed how little code is needed: around 30 lines, including the helper function. It shows off the power of CLOS, and how it’s possible to change even the basic underlying structures of the object system with relative ease. But it also shows how Lisp opens-up the space of programming styles, things that benefit from being policies that can be changed, rather than hard-coding one particular choice.

Footnotes:

1

This is also programmable when required, for example to run methods least-specific-first.

2

I got the idea for this function from method-combination-utilities, and included it literally to avoid creating another dependency.