Advice

$Revision: 5.0.2.4 $

The document introduction.htm provides an overview of the Allegro CL documentation with links to all major documents. The document index.htm is an index with pointers to every documented object (operators, variables, etc.) The revision number of this document is below the title. These documents may be revised from time to time between releases.

1.0 Introduction
2.0 Advising a function
     2.1 Advice of class :before
     2.2 Advice of class :around
     2.3 Advice of class :after
3.0 Removing advice from a function or macro
4.0 Utilities associated with advice
5.0 Examples using advice
6.0 Advising macros


1.0 Introduction

The advice facility allows you to affect the action of a function by adding code before, around or after the evaluation of forms defining a function. You may do this with interpreted or compiled functions without changing the source code or having to recompile. This facility is particularly useful in cases where you have compiled code but do not have access to the source code (for example, in a commercial application).

Advice works on macros as well as functions but what gets advised is the macro-expander function, not the forms returned by the macro expansion. Thus, advising a macro is more complex than advising a function and there are fewer situations where advising a macro is helpful. 6.0 Advising macros below describes how to advise a macro. The remainder of this document describes advising functions only.

Note that you cannot advise special forms, such as setq or progn, and that some standard Common Lisp functions that are normally compiled in-line, for example car, will not run advice code when compiled. (Since many Common Lisp functions may be compiled in-line, care should be used when advising such functions, since advice will not be run if a function is compiled inline. Compiling: Help with declarations describes how to get information during the code generation phase of compilation.)

To get the nomenclature straight:

The functions for the advice facility are in the trace module, which may not be included with the default Lisp image file. If you wish to use advice, the module must be loaded. It will be autoloaded with the first call to most advice functions, but you can load it explicitly, for example by evaluating the following form:

(require :trace)

If you commonly use advice, you may want to put such a form in your .clinit.cl file.

Advising a function has a number of uses:

  1. To put debugging information in a function for testing (checking arguments and return values, for example). Doing so is useful particularly since advice code can be very easily removed (with a call to excl:unadvise). Note that advice code is evaluated each time a function is called, so you may wish to avoid doing output in advice code in a recursive function.
  2. To add extra argument checking or conditioning to an already written function without modifying the function itself.
  3. To modify code provided by others where you do not have access to the source (and therefore cannot modify the code directly).

If a function which has been advised (that is: advice code is attached to it) is redefined with defun, either at the top level or by loading a new definition from a file, the function will still have the same advice. The advice will be removed only with a call to excl:unadvise or a related function.

Advice code can access the list of arguments to the advised function, which is bound to the variable excl:arglist, and the list of returned values, which is bound to the Lisp variable values. The advice code may look at and change either the argument list or the list of returned values. (Which values can be modified depends on the type of advice. :before and :around advice may change the argument list, but the list bound to values is not formed until all :around advice has been evaluated. :after advice has access to values, but, since the advised function has already run, the argument list is of little interest to :after advice.) If you change either list, be careful to ensure that you preserve such structure as is necessary. For example, if you replace the argument list, you must be sure the new list has as least as many elements as there are required arguments to the advised function.

There are five functions for advising and unadvising:

excl:advise
excl:advise-1
excl:unadvise
excl:unadvise-1
excl:defadvice

The following functions and global variable are also useful:

excl:advised-functions
excl:compile-advice
excl:describe-advice
excl:*compile-advice*

excl:advise and excl:unadvise are macros, excl:advise-1 and excl:unadvise-1 are the functions which implement the macros. The macro excl:defadvice allows users to use a defun-like syntax for advice.

2.0 Advising a function

You may advise functions with excl:advise, excl:advise-1 or excl:defadvice. advise and advise-1 take as arguments the name of the function being advised, the class, described next, the position (relative to other advice, see below), the name given to the piece of advice being added, and the forms to run when the function is called. Advice comes in three types, called the class of advice. These are :before, :around, and :after. Let us look more closely at the classes of advice and at the possible values for the position argument.

2.1 Advice of class :before

:before advice will be evaluated before the function call. The argument list with which the function was called is available to :before advice. The argument list is stored as the value of the variable excl:arglist. You may check the values in this list, change the values or replace the list altogether with a new list. (If you replace the list, be careful that it have the correct format for the function (number and types of arguments in the list) or you may get an error, or worse, a wrong result but no error.) :before advice is used only for its side effects. Any value returned by :before advice code is ignored.

2.2 Advice of class :around

:around advice places the function call inside the code of the advice. The keyword :do-it signals where the function should be called within the advice code. When Lisp encounters the :do-it, it calls the next piece of :around advice, if there is more, or the function. When the function returns, the :around advice code continues execution. :do-it may appear several times in :around advice. Normally, it is placed in conditional code, e.g.

(if (zerop (car excl:arglist))(+ 5 :do-it)(* 7 :do-it))

 In that case, the system will encounter one or the other :do-it, but not both. However, it is allowed to have several :do-it's, all of which are evaluated. In that case, the succeeding :around advice and the advised function are evaluated more than once. :after advice is still evaluated only once, however. :around advice can work with excl:arglist before the :do-it. Since the advised function is run at the location specified by the :do-it, the values the function return are available to :around advice just like with any function call. The list bound to the variable values is not set up until after all :around advice is run, then values is bound to the list of values returned by the final piece of :around advice. Note that if the function returns multiple values, these should be caught with multiple-value-bind or multiple-value-list or some similar function if you are interested in the values beyond the first.

2.3 Advice of class :after

:after advice is evaluated after the function has completed, but before control is passed back to whatever called the function. :after advice may examine and change the list of returned values from the last piece of :around advice (or the function, if there is no :around advice) stored in the variable values. :after advice is used only for its side effects. Any value returned by the :after advice code is ignored. The list bound to values is returned from the now completed function call.

2.4 More on the position argument

Let us look at the position argument more closely. As we said above, advice is numbered 0, 1, 2, ..., n-1, if there are n pieces of advice of a given class. Advice with a smaller index is run farther from the function call than advice with a larger index. Regardless of what position was specified for advice when it was defined, its real position is the number in the list from 0 to n-1. A new piece of advice of the same class will be placed according to its position argument. If that argument is a number, that number is placed with respect to the list 0, ..., n-1. The argument can also be a name of other advice, in which case it is placed immediately farther from the function call than that named advice. Or the argument can be nil, in which case the advice is placed farthest from the function call. Here are some examples. Let us assume that the function foo has no :before advice on it when we start. We will define advice, and show after each definition the order in which the advice will be run.

(excl:advise foo :before john 10 (format t "hello john"))

Things will be run in the following order:

john foo

(excl:advise foo :before cathy 5 (format t "hello cathy"))

Things will be run in the following order:

john cathy foo

(excl:advise foo :before bill nil (format t "hello bill"))

Things will be run in the following order:

bill john cathy foo

(excl:advise foo :before carol 1 (format t "hello carol"))

Things will be run in the following order:

bill carol john cathy foo

(excl:advise foo :before david john (format t "hello david"))

Things will be run in the following order:

bill carol david john cathy foo

(excl:advise foo :before mike 1000 (format t "hello mike"))

Things will be run in the following order:

bill carol david john cathy mike foo

(excl:advise foo :before chris 100 (format t "hello chris"))

Things will be run in the following order:

bill carol david john cathy mike chris foo

3.0 Removing advice from a function or macro

 The excl:unadvise macro and the excl:unadvise-1 function are used to remove advice from functions or macros. Note that excl:unadvise called without arguments removes all advice from all functions and macros.

Let us look at some examples of unadvising functions. Note particularly how the value nil for the arguments to excl:unadvise is interpreted.

(excl:unadvise func :before joe)

removes the :before advice named joe from func.

(excl:unadvise func :before nil)

 removes all :before advice from func.

(excl:unadvise func nil joe)

 removes all advice named joe (whether :before, :after or :around) from func.

(excl:unadvise nil :around)

 removes all :around advice from all functions.

(excl:unadvise func)

 removes all advice from func.

(excl:unadvise nil)

or

(excl:unadvise)

 removes all advice from all functions.

4.0 Utilities associated with advice

 The following functions and variables are useful when using the advice facility.

excl:advised-functions, a function, returns a list of all advice on all advised functions.

excl:*compile-advice*, a variable, if true, causes newly added advice to be compiled automatically. If nil, newly added advice is left uncompiled (advice can be compiled with compile-advice).

excl:compile-advice, a function, compiles the advice on the function named by the argument.

excl:describe-advice, a function, prints a description of advice on the function named by the required argument to the stream named by the optional argument (which defaults to *standard-output*).

5.0 Examples using advice

In the following example, we define a function and then add advice code which checks and (if necessary) modifies the arguments, signals an error for wrong arguments, and takes care of special cases not actually handled by the function itself. We do not suggest that the following example represents good programming style or that using advice is preferable to actually rewriting the original function. The example is designed only to show how advice works. We apply advice using excl:advise, excl:advise-1, and excl:defadvice.

Consider the following function, which calculates both the number of permutations of n things taken k at a time and the number of combinations of n things taken k at a time, returning both as multiple values.

(defun perm-and-comb (n k)
  (let ((perm (/ (fact n) (fact (- n k)))))
    (values perm (/ perm (fact k)))))

(defun fact (n)
  (if (= n 1) 1 (* n (fact (1- n)))))

There are several things to notice about perm-and-comb. One is that if you put the arguments in the wrong order (i.e. if the first argument is less than or equal to the second) or if either argument is not an integer, the function goes into an infinite loop. The first pieces of :before advice check that the arguments are in the right order.

(excl:advise perm-and-comb :before order nil
  (let ((n (car excl:arglist)) (k (cadr excl:arglist)))
    (if (>= k n) (setq excl:arglist (list k n)))))

The next piece of advice checks that the arguments are integers, truncating them if they are not.

(excl:advise perm-and-comb :before truncate nil
  (let ((n (car excl:arglist)) (k (cadr excl:arglist)))
    (if (not (integerp n))
        (setf (car excl:arglist) (truncate n)))
    (if (not (integerp k))
        (setf (cadr excl:arglist) (truncate k)))))

(We are not suggesting that it is good programming style to change arguments without warning or notice. Generally, that is a bad idea.)

All the cases where numeric arguments to perm-and-comb cause an infinite loop have not yet been covered. The following :around advice completely checks the arguments (assuming that they are numbers), causing a fatal error if the arguments will cause an infinite loop. Some of the checks are redundant, of course, given the :before advice already applied to perm-and-comb.

(excl:defadvice perm-and-comb (error :around)
  (let ((n (car excl:arglist)) (k (cadr excl:arglist)))
    (if (or (not (integerp n)) (not (integerp k))
            (>= k n) (> 1 k))
        (error "Improper arguments to comb-and-perm: ~D ~D" n k)
        :do-it)))

Now one last change. The form (perm-and-comb 10 0) will generate an error (and, indeed will cause an infinite loop, since (fact 0) will not stop), but the number of permutations and the number of combinations of 10 things taken none at a time is in fact defined. (There is only one way: choose nothing.) Let us add one final piece of :around advice, which will return 1 and 1 when the argument k is 0. This advice must run before the error advice defined above, since the argument k being 0 will cause a fatal error if the error advice sees it. By specifying a position of error, we guarantee that this new advice is placed farther from the actual function call than the error advice. Note too that the forms argument is contained in a list.

(excl:advise-1 'perm-and-comb :around 'zerotest 'error
  '((let ((k (cadr excl:arglist)))
      (if (zerop k) (return (values 1 1)) :do-it))))

Notice the use of return. Since we know the answer, there is no reason to call the function.

For the final example, let us suppose that you are actually interested in the number of permutations and the ratio of permutations to combinations. Here is :after advice that will change the returned values to be what you want. The first returned value is left alone and the second is replaced by the ratio of the first and the original second.

(excl:advise perm-and-comb :after nil nil
  (let ((p (car values)) (c (cadr values)))
    (setq values (list p (/ p c)))))

Note that if the :around advice named zerotest returns without calling perm-and-comb, this :after advice will not be run. In this case, that is okay, since the values returned by zerotest would not be changed by the :after advice. However, it is important to keep in mind all of the advice on a particular function to ensure that different pieces are not working at cross purposes. The function excl:describe-advice, defined above, is useful for that purpose. When all of the advice given above has been attached to perm-and-comb, describe-advice prints the following.  

USER(21): (let ((*print-pretty* t))
           (excl:describe-advice 'perm-and-comb))
Before advice:
(truncate
  (let ((n (car arglist))
        (k (cadr arglist)))
    (if (not (integerp n)) (setf (car arglist) (truncate n)))
    (if (not (integerp k)) (setf (cadr arglist) (truncate k)))))
(order
  (let ((n (car arglist))
        (k (cadr arglist)))
    (if (>= k n) (setq arglist (list n k)))))
Around advice:
(zerotest 
  (let ((k (cadr arglist)))
    (if (= k 0) (return (values 1 1)) :do-it)))
(error
  (let ((n (car arglist))
        (k (cadr arglist)))
    (if (or (not (integerp n)) (not (integerp k))
            (>= k n) (> 1 k))
        (error "Improper arguments to comb-and-perm: ~D ~D" n k)
        :do-it)))
After advice:
(nil 
  (let ((p (car values)
      (c (cadr values)))
    (setq values (list p (/ p c)))))

6.0 Advising macros

You may advise macros as well as functions but note that both what is being advised and where the advice is run is very different when a macro is advised than when a function is advised. When you advise a macro, you advise the macro expansion function. This function takes two arguments, the whole form and the environment. The macro name will be the car of the form argument. The advice will thus be run at macro expansion time. Macros are typically advised for purposes of source-file recording, tracing, and other informational purposes. If you advise macros in order to affect their behavior (in ways other than simply printing or storing information) you must be very careful not to break or confuse the macro expansion system.

The effect and behavior of advice applied to macros (and placed on the macro expansion function) is the same as that described above for functions. Therefore, name, position, and class are all interpreted the same way and the advice is removed with excl:unadvise (or excl:unadvise-1) just as with functions.

Here is a simple example. We define the macro safe-car. Then we advise it to print a message whenever it is expanded.

USER(25): (defmacro safe-car (x)
            (let ((object (gensym)))
               (let ((,object ,x))
                 (if (consp ,object) (car ,object) ,object))))
safe-car
USER(26): (safe-car '(1 2))
1
USER(27): (advise safe-car :before expand-notify nil
              (let ((mac (caar excl:arglist)))
                (format t "The macro ~S has been expanded!~%" mac)))
safe-car

;; When we call SAFE-CAR, the macro is expanded.

USER(28): (safe-car '(1 2))
The macro safe-car has been expanded!

1
USER(29):

Copyright (C) 1998-1999, Franz Inc., Berkeley, CA. All Rights Reserved.