|Allegro CL version 8.1|
Unrevised from 8.0 to 8.1.
This document contains the following sections:1.0 The Fwrap Facility
This document describes the new fwrap (Function WRAPper) facility. Fwrappers are intended to replace the advice facility, which is still supported and also documented here. Both facilities allow runtime modification of defined functions without actually redefining the function.
The Fwrap facility is provided, starting in release 6.0, to better implement encapsulation, tracing, and advice functionalities. Highlights of the Fwrap facility include:
The encapsulation facility, as manifested in previous versions of Allegro CL in the advice and trace facilities, has a major drawback, in that the act of encapsulationg a function produces a new, non-eq function object that is installed at the point of the function spec.
This means that for example if #'foo were encapsulated, but had been previously captured and stored in a variable to be later funcalled, the funcalled function does not execute the encapsulating functionality. In practical terms, it means that it is impossible to trace a function object by anything other than its name, and it is thus also impossible to trace a function which cannot be accessed via a function spec (usually a symbol, but possibly a list; see def-function-spec-handler). Also, an internal function such as #'(flet foo bar) which is traced does not retain its trace status when the parent function (i.e. #'foo, in this case) is redefined either by defun, compile, or loading a compiled file.
Another major problem with encapsulations is that it is forms-based, and so acts in the dynamic environment only, while fwrappers operate in the lexical environment. One of the differences is internal, and affects performance rather than coding: to use advice, the argument list must be created so the advice can operate on it. The fwrap facility may not need the arguments list, and if it does not, it will not be created, saving execution time. Further, the arguments are known to advice only from the list: you cannot refer to arguments by name, only by position, and if optional or keywords arguments are defaulted, advice will not know the value (except perhaps by calling the excl:arglist function). Fwrappers can refer to arguments by name because they can be defined with the argument list specified.
Advice also defines the code that does the work and what this code is applied to at once. there is no way to define advice and apply it later to a function. Fwappers can be defined independently of what they might modify (with def-fwrapper) and then apply it to a function (with fwrap) when and only when desired. Fwrapper objects survive dumplisp and can be applied when an image is started.
The term "fwrapper" means "function wrapper". A normal function that implements any functionality is called a primary function for the purposes of this description. An fwrapper, then, is a function that "wraps" the primary function, i.e. it executes at the point of call to the primary function, but its functionality surrounds the call to the actual primary function call, in a similar way that around methods surround the primary method in CLOS. Several fwrappers may wrap a primary function; they are executed in an outer-to-inner order that can be manipulated.
During the time of the execution of fwrapped functions, an attempt is made to preserve the argument list without reproducing it, if possible, in order to reduce consing and maintain speed. The conceptual arglist starts out as a stack pointer and a number-of-arguments variable, and is only converted to a list if any one of the fwrappers in the line forces the argument list to list form. Once an argument-list is made into a list, it remains a list until the primary function is called, when it is re-expanded on the stack.
You create an fwrapper object with the def-fwrapper macro. This macro associates an fwrapper object with a name (a function spec, which is typically a symbol but can be a list), but that name does not name a function or function object and nothing is wrapped with a call to def-fwrapper. You wrap a function independently with a call to fwrap. The function being wrapped is called the primary function.
The fwrapper object can be compiled, just like any operator, with compile.
As we said above, fwrappers wrap around their primary functions and a wrapper must call call-next-fwrapper in order that the primary function (or the next inner wrapper) be called. The return values from the primary function (or the next inner wrapper) are returned from the call to call-next-fwrapper.
Consider the following simple example.
;; We define Function bar, which prints a message ;; saying bar has been called and what its argument was. user(39): (defun bar (x) (format t "Function bar called with argument ~S.~%" x)) bar ;; We define an fwrapper object. Note that bar is not ;; specifically mentioned. user(40): (def-fwrapper bar-wrap (x) (format t "Fwrapper bar-wrap called with argument ~S.~%" x) (if (integerp x) (call-next-fwrapper)) (format t "This form in bar-wrap called!~%")) bar-wrap ;; bar is not yet wrapped, so it is unaffected: user(41): (bar 10) Function bar called with argument 10. nil ;; Now we wrap bar with bar-wrap. barwrapper1 is a symbol ;; which serves as the identifier (called the indicator) ;; of this wrapper of bar. user(42): (fwrap 'bar 'barwrapper1 'bar-wrap) #<Interpreted Function bar> ;; We now call bar with argument 10. Note that the wrapper ;; executes before and after the call to call-next-fwrapper. user(43): (bar 10) Fwrapper bar-wrap called with argument 10. Function bar called with argument 10. This form in bar-wrap called! nil ;; We call bar with a non-integer argument. The conditional ;; (integerp x) fails so call-next-fwrapper, and thus bar, ;; are not called. user(44): (bar t) Fwrapper bar-wrap called with argument t. This form in bar-wrap called! nil ;; We unwrap bar: user(45): (funwrap 'bar 'barwrapper1) #<Interpreted Function bar> ;; And now bar behaves as it did when first defined. user(46): (bar t) Function bar called with argument t. nil ;; Finally we compile bar and bar-wrap, just to show ;; how wrappers are compiled: user(47): (compile 'bar) bar nil nil user(48): (compile 'bar-wrap) bar-wrap nil nil user(49): (fwrap 'bar 'barwrapper1 'bar-wrap) #<Function bar> user(50): (bar t) Fwrapper bar-wrap called with argument t. This form in bar-wrap called! nil user(51):
;; Here is an example of fwrapping a macro. ;; ;; Here is the macro definition: (defmacro safe-car (x) (let ((object (gensym))) `(let ((,object ,x)) (if (consp ,object) (car ,object) ,object)))) ;; Here is the fwrapper definition: (def-fwrapper wrap-safe-car (&rest args) (let ((mac (caar args))) (format t "The macro ~S has been expanded!~%" mac) (format t "The arglist is ~S~%" args)) (call-next-fwrapper)) ;; user(66): (safe-car '(1 2)) 1 user(67) (fwrap 'safe-car 'wsc-1 'wrap-safe-car) <Interpreted function safe-car @ #x20f1cc21> user(68) (safe-car '(1 2)) The macro safe-car has been expanded! The arglist is ((safe-car '(1 2)) nil) 1 user(68):
def-fwrapper has been designed to work well as a Common Lisp defining form. As a macro, it can be macroexpanded, thus revealing portions of the implementation of the definition it is creating. Because it uses the same techniques as defun and defmacro to create a definition, compilation of a form is efficient in resource-usage, and loading a .fasl file with a compiled def-fwrapper form does not require the compiler to be present, as is the case with compiled defadvice forms.
To find out what a def-fwrapper form is doing, it is recommended to use a standard macroexpansion technique:
(pprint (macroexpand '(def-fwrapper foo-wrap ...)))
which will reveal the implementation of foo-wrap.
To use an fwrapper that is compiled, without requiring the compiler in the image, simply place the def-fwrapper form into a source file, compile the file in a lisp with a compiler, and load the resulting fasl file into a lisp (not necessarily containing a compiler). The fwrapper will then be defined as a compiled function, and fwrap may then be used to fwrap the primary function.
We start with a simple example where a simple fwrapper is defined for wrapping a definition of a factorial function. Note that before the fwrap call, the fwrapper has no effect on the execution of fact.
user(1): (defun fact (n) (if (= n 0) 1 (* n (fact (1- n))))) fact user(2): (def-fwrapper factx (n) (when (= (mod n 2) 1) (format t "in trace, n= ~d~%" n)) (call-next-fwrapper)) factx user(3): (fact 10) 3628800 user(4): (fwrap 'fact :my-trace 'factx) #<Interpreted Function fact> user(5): (fact 10) in trace, n= 9 in trace, n= 7 in trace, n= 5 in trace, n= 3 in trace, n= 1 3628800 user(6):
There have been many requests for arglist access in trace. def-fwrapper allows the lambda list to match that of the primary function, in which case arguments can be accessed by name.
(defun foo (x y &optional z &rest args) ...) (def-fwrapper foo-wrap (x y &optional z &rest args) ...)
or, if it is desired to work with the arguments as a list, they can be specified as such:
(def-fwrapper foo-wrap (&rest arglist) ...)
The following example shows how trace and fwrap can work together and how fwrap-order can be manipulated to govern the interaction. It also demonstrates the use of describe on fwrapped functions. Of course, this example is not a good example of modular design, but its intent is to show how such interdependence behaves.
user(1): (defvar *fact-arg* 0) *fact-arg* user(2): (defun fact (num) (if (equal num 0) 1 (* num (fact (- num 1))))) fact user(3): (def-fwrapper factwrap (num) (setq *fact-arg* num) (call-next-fwrapper)) factwrap user(4): (fwrap 'fact :my-advice 'factwrap) #<Interpreted Function fact> user(5): :trace (fact :condition (= *fact-arg* 3)) (fact) user(6): (fact 12) 0: (fact 2) 0: returned 2 479001600 user(7): (describe #'fact) #<Interpreted Function fact> is a function. The arguments are (num) It has the following indicator/fwrapper pairs, from outer to inner: :trace #<Closure trace-fwrapper @ #x30c601ea> :my-advice #<Interpreted Function factwrap> user(8): (fwrap-order #'fact :outer :my-advice) (:my-advice #<Interpreted Function factwrap> :trace #<Closure trace-fwrapper @ #x30c601ea>) user(9): (describe #'fact) #<Interpreted Function fact> is a function. The arguments are (num) It has the following indicator/fwrapper pairs, from outer to inner: :my-advice #<Interpreted Function factwrap> :trace #<Closure trace-fwrapper @ #x30c601ea> user(10): (fact 12) 0: (fact 3) 0: returned 6 479001600 user(11):
The only change that has been made to the trace interface is the addition of the :not-inside trace option. It is similar to the :inside option, in that it checks whether or not the specified function is active when the traced function is entered. However, it requires that the specified function not be on the stack. Both :inside and :not-inside work together and check simultaneously, for maximum efficiency.
Other changes that are not functional are mostly bug fixes and enhancements to the operation of trace:
We recommend that you replace any advice code you might have with fwrapper code (though advice is still supported). This section indicates how such code might be rewritten.
Things to note:
Here is an example from the advice examples section below with the corresponding fwrapper code.
;; First we define a function to be advised. (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))))) ;; This before advice: (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))))) ;; would be replaced with this fwrapper (note the arguments ;; are specified and can be refered to by name): (def-fwrapper order-wrap (n k) (if (>= k n) (let ((hold n)) (setq n k) (setq k hold))) (call-next-fwrapper)) ;; and applied with a call to fwrap: (fwrap 'perm-and-comb 'w1 'order-wrap) ;; This around advice: (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))) ;; would be replaced with this fwrapper: (def-fwrapper arg-check-wrap (n k) (if (or (not (integerp n)) (not (integerp k)) (>= k n) (> 1 k)) (error "Improper arguments to comb-and-perm: ~D ~D" n k)) (call-next-fwrapper)) ;; and applied with (fwrap 'perm-and-comb 'w2 'arg-check-wrap) ;; but we are not done. arg-check-wrap is now outside ;; order-wrap, but it should be inside (around advice was ;; automatically inside before advice) so we have to ;; call fwrap-order (the value of the :inner argument is ;; placed innermost): (fwrap-order 'perm-and-comb :inner 'w2)
The advice facility has been replaced with the fwrapper facility, described above in the document. Advice is still supported, and the remainder of this document is the (somewhat abreviated) description of advice and advising functions from the release 5.0.1 and earlier documentation.
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. Section 6.5 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:
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:
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 unadvise or a related function.
Advice code can access the list of arguments passed 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:
The following functions and global variable are also useful:
advise and unadvise are macros, advise-1 and unadvise-1 are the functions which implement the macros. The macro defadvice allows users to use a defun-like syntax for advice.
You may advise functions with advise, advise-1 or 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.
: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.
: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
: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
: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
: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.
:after advice is evaluated after the function has
completed, but before control is passed back to whatever called the
:after advice may examine and change the list
of returned values from the last piece of
(or the function, if there is no :around advice) stored in the
: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
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.
(defun foo (&rest args) nil) (excl:advise foo :before john 10 (format t "hello john"))
Things will be run in the following order:
(excl:advise foo :before cathy 5 (format t "hello cathy"))
Things will be run in the following order:
john cathy foo
The unadvise macro and the unadvise-1 function are used to remove advice from functions or macros. Note that 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 unadvise is
(excl:unadvise func :before joe)
:before advice named joe from
(excl:unadvise func :before nil)
:before advice from func.
(excl:unadvise func nil joe)
removes all advice named joe (whether
(excl:unadvise nil :around)
:around advice from all functions.
removes all advice from func.
removes all advice from all functions.
The following functions and variables are useful when using the advice facility.
advised-functions, a function, returns a list of all advice on all advised functions.
variable, if true, causes newly added advice to be compiled automatically. If
newly added advice is left uncompiled (advice can be compiled with compile-advice).
compile-advice, a function, compiles the advice on the function named by the argument.
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
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 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
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)))))
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 unadvise (or 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-2009, Franz Inc. Oakland, CA., USA. All rights reserved.
Documentation for Allegro CL version 8.1. This page was not revised from the 8.0 page.
|Allegro CL version 8.1|
Unrevised from 8.0 to 8.1.