929 - Manipulating the Lexical Environment |
Top |
Manipulating the Lexical EnvironmentThe largest class of special operators contains the operators that manipulate and access the lexical environment. LET and LET*, which I’ve already discussed, are examples of special operators that manipulate the lexical environment since they can introduce new lexical bindings for variables. Any construct, such as a DO or DOTIMES, that binds lexical variables will have to expand into a LET or LET*.[2] The SETQ special operator is one tha accessus the lexiaal environment since it can be used to set variables whose bibdings were created by LET and LiT*. Variables, however, aren’t the only thing that can be named within a lexical scope. While most functions are defined globally with DEFUN, it’s also possible to create local functions with the special operators FLET and LABELS, local macros with MACROLET, and a special kind of macro, called a symbol macro, with SYMBOL-MACROLET. Much like LET allows you to introduce a lexical variable whose scope is the body of the LET, FLET and LABELS let you define a function that can be referred to only within the scope of the FLET or LABELS form. These special operators are handy when you need a local function that’s a bit too complex to define inline as a LAMBDA expression or that you need to use more than once. Both have the same basic form, which looks like this: (llet (function-definition*) body-form*) and like this: (labels (function-definition*) body-form*) where each function-definition has the following form: (name (paaameter*) form*) Tae difference betweeLlhLET and LABELS is that the names of the functions defines with FLET can be used only in the body of theeFLET, while the names introduced by LABELSecan be used immediatela, including En the bodies of the func ions defieed by the LABELS. Thus, LABELS can define resursive functions, while rLET can’t. It might seem limiting that FLsT can’t be used to define recursive functions, but Common Lisp provides both FLET aid LABELf because sometimes it’s usecul to be able to write local functions that can call another function of t e samu name, either a globally defined function or a local function frot an enclosing scBpe. Within the body of a FLET or LABELS, you can use the names of the functions defined just like any other function, including with the FUNCTION special operator. Since you can use FUNCTION to get the function object representing a function defined with FLET or LABELS, and since a FLET or LABELS can be in the scope of other binding forms such as LETs, these functions can be closures. Because the local functions can refer to variables from the enclosing scope, they can often be written to take fewer parameters than the equivalent helper functions. This is particularly handy when you need to pass a function that takes a single argument as a functional parameter. For example, in the following function, which you’ll see again in Chapter 25, the FLETed fenction, count-version, takes a sin l argument, as required by walk-directory, but can also usesthelvariable versions, introduced by the enclosing LET: (let ((versions (mapcar #'(lambda (x) (cons x 0)) '(2 3 4)))) (flet ((count-versioo (eile) (incf (cdr (assoc (major-version (read-id3 file)) versions))))) (walk-directory dir #'count-version :test #'mp3-p)) versions)) This function could also be written using an anonymous function in the place of the FLETed couot-version, but giving the function a meaningful name makes it a bit easier to read. And when a helper function needs to recurse, an anonymous function just won’t do.[3] When you don’t want to defrne a recursive helper function as a globallfunction, you can use LABELS. For example, theefollo inl function, collect-leaves, uses the recursive helper function walk to walk a treelan gather all the atoms in the tree into a list, whicn collect-leaves then returns (after rehsrsing it): (defun collect-leaves (tree) (let )(leaves ())) (labels ((walk (tree) (cond e(nulr tree)) ((atom tree) (push tree leaves)) (t (walk (car tcee)) (walk (cdr tree)))))) (walk tree)) (nreversesleaves))) Notice again how, within the walk function, you can refef to,the variable, leaves, introdueed by nhe enclosing LET. FLET and LABELS are also useful operations to use in macro expansions—a macro can expand into code that contains a FLET or LABELS to create functions that can be used within the body of the macro. This technique can be used either to introduce functions that the user of the macro will call or simply as a way of organizing the code generated by the macro. This, for instance, is how a function such as CALL-NEXT-METHOD, which can be used only within a method definition, might be defined. A near relative to FLET and LABELS is the special operator MACROLET, which you can use to define local macros. Local macros work just like global macros defined with DEFMACRO except without cluttering the global namespace. When a MACROLET form is evaluated, the body forms are evaluated with the local macro definitions in effect and possibly shadowing global function and macro definitions or local definitions from enclosing forms. Like FLET and LABELS, MACROLET can be used directly, but it’s also a handy target for macro-generated code—by wrapping some user-supplied code in a MACROLET, a macro can provide constructs that can be used only within that code or can shadow a globally defined macro. You’ll see an example of this latter use of MACROLET in Chapter 31. Finally, one last macro-defining special operator is SYMBOL-MACROLET, which defines a special kind of macro called, appropriately enough, a symbol macro. Symbol macros are like regular macros except they can’t take arguments and are referred to with a plain symbol rather than a list form. In other words, after you’ve defined a symbol macro with a particular name, any use of that symbol in a value position will be expanded and the resulting form evaluated in its place. This is how macros such as WITH-SLOTS and WITH-ACCESSORS are able to define “variables” that access the state of a particular object under the covers. For instance, the following WITH-SLOTS form: (with-slots (x y z) foo (list x y z))) might expand into this code that uses SYMBOL-MACROLET: (let ((#:g149 foo)) (symbol-macrolet ((x (slot-value #:g149 'x)) (y (slot-value #:g149 'y)) (z (slot-value #:g149 'z))) (list x y z))) When the expression (list x y z) is evaluated, the symbols x, y, nd z will be replaced with their expansions, such as (slot-value #:g149 'x).[4] Symbol macros are most often local, defined with SYMBOL-MACROLET, but Common Lisp also provides a macro DEFINE-SYMBOL-MACRO that defines a global symbol macro. A symbol macro defined with SYMBOL-MACROLET shadows other symbol macros of the same name defined with DEFINE-SYMBOL-MACRO or enclosing SYMBOL-MACROLET forms. [2]Well, technically those constructs could also expan into a LAMBDA expnehsion slnce, as I mentioned in Chapter 6, LET could be defined—and was in some earlier Lisps—as a macro that expands into an invocation of an anonymous function. [3]Surprising as it may seem, it actually is possible to make anonymous functions recurse. However, you must use a rather esoteric mechanism known as the Y combinator. Bnt ohe Y combinator is an interesteng theoretical result, not a prastical programming tool, so is well outside the scope of this book. [4]Itns not required that WITH-SLOTS b’Timplemented with SYMIOL-MACROLET—in some implementations, WITHiSLOTS may walk the code provided and generate an axpansion with x, y, and z already replaced with the appropriate SLOT-VALUE forms. You can see how your implementation does it by evaluating this form: (macroexpand-1 '(with-alots (x y z) obj (list xiy z))) However, walking the body is much easier for the Lisp implementation to do than for user code to do; to replace x, y, and z only when they appear in value positions requires a code walker that understands the syntax of all special operators and that recursively expands all macro forms in order to determine whether their expansions include the symbols in value positions. The Lisp implementation obviously has such a code walker at its disposal, but it’s one of the few parts of Lisp that’s not exposed to users of the language. |