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;
-
:beforeand:aftermethods that run before and after the primary methods; -
:aroundmethods that run around the:before-primary-:aftercombination; and -
:ifmethods that act as guards, running before any:aroundmethods 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:
This is also programmable when required, for example to run methods least-specific-first.
I got the idea for this function from method-combination-utilities, and included it literally to avoid creating another dependency.