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 function 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.)