• 沒有找到結果。

Applications for Macros

在文檔中 Bottom-upDesign Preface (頁 120-127)

When to Use Macros

8.3 Applications for Macros

> (funcall #’(lambda (x y) (avg x y)) 1 3) 2

However, this is an inconvenience. It doesn’t always work, either: even if, like avg, the macro has an &rest parameter, there is no way to pass it a varying number of arguments.

5. Clarity of source code. Macro definitions can be harder to read than the equivalent function definitions. So if writing something as a macro would only make a program marginally better, it might be better to use a function instead.

6. Clarity at runtime. Macros are sometimes harder to debug than functions.

If you get a runtime error in code which contains a lot of macro calls, the code you see in the backtrace could consist of the expansions of all those macro calls, and may bear little resemblance to the code you originally wrote.

And because macros disappear when expanded, they are not accountable at runtime. You can’t usually use trace to see how a macro is being called.

If it worked at all, trace would show you the call to the macro’s expander function, not the macro call itself.

7. Recursion. Using recursion in macros is not so simple as it is in functions.

Although the expansion function of a macro may be recursive, the expansion itself may not be. Section 10.4 deals with the subject of recursion in macros.

All these considerations have to be balanced against one another in deciding when to use macros. Only experience can tell which will predominate. However, the examples of macros which appear in later chapters cover most of the situations in which macros are useful. If a potential macro is analogous to one given here, then it is probably safe to write it as such.

Finally, it should be noted that clarity at runtime (point 6) rarely becomes an issue. Debugging code which uses a lot of macros will not be as difficult as you might expect. If macro definitions were several hundred lines long, it might be unpleasant to debug their expansions at runtime. But utilities, at least, tend to be written in small, trusted layers. Generally their definitions are less than 15 lines long. So even if you are reduced to poring over backtraces, such macros will not cloud your view very much.

8.3 Applications for Macros

Having considered what can be done with macros, the next question to ask is:

in what sorts of applications can we use them? The closest thing to a general

description of macro use would be to say that they are used mainly for syntactic transformations. This is not to suggest that the scope for macros is restricted.

Since Lisp programs are made from1lists, which are Lisp data structures, “syn-tactic transformation” can go a long way indeed. Chapters 19–24 present whole programs whose purpose could be described as syntactic transformation, and which are, in effect, all macro.

Macro applications form a continuum between small general-purpose macros like while, and the large, special-purpose macros defined in the later chapters. On one end are the utilities, the macros resembling those that every Lisp has built-in.

They are usually small, general, and written in isolation. However, you can write utilities for specific classes of programs too, and when you have a collection of macros for use in, say, graphics programs, they begin to look like a programming language for graphics. At the far end of the continuum, macros allow you to write whole programs in a language distinctly different from Lisp. Macros used in this way are said to implement embedded languages.

Utilities are the first offspring of the bottom-up style. Even when a program is too small to be built in layers, it may still benefit from additions to the lowest layer, Lisp itself. The utility nil!, which sets its argument to nil, could not be defined except as a macro:

(defmacro nil! (x)

‘(setf ,x nil))

Looking at nil!, one is tempted to say that it doesn’t do anything, that it merely saves typing. True, but all any macro does is save typing. If one wants to think of it in these terms, the job of a compiler is to save typing in machine language. The value of utilities should not be underestimated, because their effect is cumulative: several layers of simple macros can make the difference between an elegant program and an incomprehensible one.

Most utilities are patterns embodied. When you notice a pattern in your code, consider turning it into a utility. Patterns are just the sort of thing computers are good at. Why should you bother following them when you could have a program do it for you? Suppose that in writing some program you find yourself using in many different places do loops of the same general form:

(do ()

((not condition)) . body of code)

1Made from, in the sense that lists are the input to the compiler. Functions are no longer made of lists, as they used to be in some earlier dialects.

8.3 APPLICATIONS FOR MACROS 113

When you find a pattern repeated through your code, that pattern often has a name.

The name of this pattern is while. If we want to provide it in a new utility, we will have to use a macro, because we need conditional and repeated evaluation. If we define while using this definition from page 91:

(defmacro while (test &body body)

‘(do ()

((not ,test)) ,@body))

then we can replace the instances of the pattern with (while condition

. body of code)

Doing so will make the code shorter and also make it declare in a clearer voice what it’s doing.

The ability to transform their arguments makes macros useful in writing in-terfaces. The appropriate macro will make it possible to type a shorter, simpler expression where a long or complex one would have been required. Although graphic interfaces decrease the need to write such macros for end users, program-mers use this type of macro as much as ever. The most common example is defun, which makes the binding of functions resemble, on the surface, a function definition in a language like Pascal or C. Chapter 2 mentioned that the following two expressions have approximately the same effect:

(defun foo (x) (* x 2)) (setf (symbol-function ’foo)

#’(lambda (x) (* x 2)))

Thus defun can be implemented as a macro which turns the former into the latter.

We could imagine it written as follows:2

(defmacro our-defun (name parms &body body)

‘(progn

(setf (symbol-function ’,name)

#’(lambda ,parms (block ,name ,@body)))

’,name))

Macros like while and nil! could be described as general-purpose utilities.

Any Lisp program might use them. But particular domains can have their utilities

2For clarity, this version ignores all the bookkeeping that defun must perform.

(defun move-objs (objs dx dy)

(multiple-value-bind (x0 y0 x1 y1) (bounds objs) (dolist (o objs)

(incf (obj-x o) dx) (incf (obj-y o) dy))

(multiple-value-bind (xa ya xb yb) (bounds objs) (redraw (min x0 xa) (min y0 ya)

(max x1 xb) (max y1 yb))))) (defun scale-objs (objs factor)

(multiple-value-bind (x0 y0 x1 y1) (bounds objs) (dolist (o objs)

(setf (obj-dx o) (* (obj-dx o) factor) (obj-dy o) (* (obj-dy o) factor))) (multiple-value-bind (xa ya xb yb) (bounds objs)

(redraw (min x0 xa) (min y0 ya) (max x1 xb) (max y1 yb)))))

Figure 8.1: Original move and scale.

as well. There is no reason to suppose that base Lisp is the only level at which you have a programming language to extend. If you’re writing aCADprogram, for example, the best results will sometimes come from writing it in two layers: a language (or if you prefer a more modest term, a toolkit) forCADprograms, and in the layer above, your particular application.

Lisp blurs many distinctions which other languages take for granted. In other languages, there really are conceptual distinctions between compile-time and runtime, program and data, language and program. In Lisp, these distinctions exist only as conversational conventions. There is no line dividing, for example, language and program. You can draw the line wherever suits the problem at hand. So it really is no more than a question of terminology whether to call an underlying layer of code a toolkit or a language. One advantage of considering it as a language is that it suggests you can extend this language, as you do Lisp, with utilities.

Suppose we are writing an interactive 2Ddrawing program. For simplicity, we will assume that the only objects handled by the program are line segments, represented as an originx,y and a vector dx,dy. One of the things such a program will have to do is slide groups of objects. This is the purpose of the function move-objs in Figure 8.1. For efficiency, we don’t want to redraw the whole screen after each operation—only the parts which have changed. Hence

8.3 APPLICATIONS FOR MACROS 115

(defmacro with-redraw ((var objs) &body body) (let ((gob (gensym))

(x0 (gensym)) (y0 (gensym)) (x1 (gensym)) (y1 (gensym)))

‘(let ((,gob ,objs))

(multiple-value-bind (,x0 ,y0 ,x1 ,y1) (bounds ,gob) (dolist (,var ,gob) ,@body)

(multiple-value-bind (xa ya xb yb) (bounds ,gob) (redraw (min ,x0 xa) (min ,y0 ya)

(max ,x1 xb) (max ,y1 yb))))))) (defun move-objs (objs dx dy)

(with-redraw (o objs) (incf (obj-x o) dx) (incf (obj-y o) dy))) (defun scale-objs (objs factor)

(with-redraw (o objs)

(setf (obj-dx o) (* (obj-dx o) factor) (obj-dy o) (* (obj-dy o) factor))))

Figure 8.2: Move and scale filleted.

the two calls to the function bounds, which returns four coordinates (min x, min y, max x, max y) representing the bounding rectangle of a group of objects. The operative part of move-objs is sandwiched between two calls to bounds which find the bounding rectangle before and then after the movement, and then redraw the entire affected region.

The function scale-objs is for changing the size of a group of objects.

Since the bounding region could grow or shrink depending on the scale factor, this function too must do its work between two calls to bounds. As we wrote more of the program, we would see more of this pattern: in functions to rotate, flip, transpose, and so on.

With a macro we can abstract out the code that these functions would all have in common. The macro with-redraw in Figure 8.2 provides the skeleton that the functions in Figure 8.1 share.3 As a result, they can now be defined in four lines each, as at the end of Figure 8.2. With these two functions the new macro has already paid for itself in brevity. And how much clearer the two functions

3The definition of this macro anticipates the next chapter by using gensyms. Their purpose will be explained shortly.

become once the details of screen redrawing are abstracted away.

One way to view with-redraw is as a construct in a language for writing interactive drawing programs. As we develop more such macros, they will come to resemble a programming language in fact as well as in name, and our application itself will begin to show the elegance one would expect in a program written in a language defined for its specific needs.

The other major use of macros is to implement embedded languages. Lisp is an exceptionally good language in which to write programming languages, because Lisp programs can be expressed as lists, and Lisp has a built-in parser (read) and compiler (compile) for programs so expressed. Most of the time you don’t even have to call compile; you can have your embedded language compiled implicitly, by compiling the code which does the transformations (page 25).

An embedded language is one which is not written on top of Lisp so much as commingled with it, so that the syntax is a mixture of Lisp and constructs specific to the new language. The naive way to implement an embedded language is to write an interpreter for it in Lisp. A better approach, when possible, is to implement the language by transformation: transform each expression into the Lisp code that the interpreter would have run in order to evaluate it. That’s where macros come in. The job of macros is precisely to transform one type of expression into another, so they’re the natural choice when writing embedded languages.

In general, the more an embedded language can be implemented by transfor-mation, the better. For one, it’s less work. If the new language has arithmetic, for example, you needn’t face all the complexities of representing and manipu-lating numeric quantities. If Lisp’s arithmetic capabilities are sufficient for your purposes, then you can simply transform your arithmetic expressions into the equivalent Lisp ones, and leave the rest to the Lisp.

Using transformation will ordinarily make your embedded languages faster as well. Interpreters have inherent disadvantages with respect to speed. When code occurs within a loop, for example, an interpreter will often have to do work on each iteration which in compiled code could have been done just once. An embedded language which has its own interpreter will therefore be slow, even if the interpreter itself is compiled. But if the expressions in the new language are transformed into Lisp, the resulting code can then be compiled by the Lisp compiler. A language so implemented need suffer none of the overheads of interpretation at runtime.

Short of writing a true compiler for your language, macros will yield the best performance. In fact, the macros which transform the new language can be seen as a compiler for it—just one which relies on the existing Lisp compiler to do most of the work.

We won’t consider any examples of embedded languages here, since Chap-ters 19–25 are all devoted to the topic. Chapter 19 deals specifically with the

dif-8.3 APPLICATIONS FOR MACROS 117

ference between interpreting and transforming embedded languages, and shows the same language implemented by each of the two methods.

One book on Common Lisp asserts that the scope for macros is limited, citing as evidence the fact that, of the operators defined inCLTL1, less than 10% were macros. This is like saying that since our house is made of bricks, our furniture will be too. The proportion of macros in a Common Lisp program will depend entirely on what it’s supposed to do. Some programs will contain no macros.

Some programs could be all macros.

在文檔中 Bottom-upDesign Preface (頁 120-127)