Is there an advantage to this macro?

163 views Asked by At

I am reading Practical Common Lisp by Peter Seibel. In Chapter 9, he is walking the reader through creating a unit testing framework, and he includes the following macro to determine whether a list is composed of only true expressions:

(defmacro combine-results (&body forms)
  (let ((result (gensym)))
    `(let ((,result t))
       ,@(loop for form in forms collect `(unless ,form (setf ,result nil)))
       ,result)))

I'm not clear what the advantage to using a macro is here, though - it seems like the following would be clearer, as well as more efficient for dynamic values:

(defun combine-results (&rest expressions)
  (let ((result t))
    (loop for expression in expressions do (unless expression (setf result nil)))
    result))

Is the advantage to the macro just that it's more efficient at runtime for any calls that are expanded at compile-time? Or is it a paradigm thing? Or is the book just trying to give excuses to practice different patterns in macros?

2

There are 2 answers

5
Kaz On BEST ANSWER

Your observation is basically right; and in fact your function can just be:

(defun combine-results (&rest expressions)
  (every #'identity expressions))  ;; i.e. all expressions are true?

Since the macro unconditionally evaluates all of its arguments from left to right, and yields T if all of them are true, it is basically just inline-optimizing something which can be done by a function. Functions can be requested to be inlined with (declaim 'inline ...). Moreover, we can write a compiler macro for the function anyway with define-compiler-macro. With that macro we can produce the expansion, and have this as a function that we can apply and otherwise indirect upon.

Other ways of calculating the result inside the function:

(not (position nil expressions))
(not (member nil expressions))

The example does look like macro practice: making a gensym, and generating code with loop. Also, the macro is the starting point for something that might appear in a unit testing framework.

0
Rainer Joswig On

In this case it might not matter, but for future versions it might be more useful to use a macro. Does it make sense to use a macro? Depends on the use case:

Using a function

(combine-results (foo) (bar) (baz))

Note that at runtime Lisp sees that combine-results is a function. It then evaluates the arguments. It then calls the function combine-results with the result values. This evaluation rule is hardcoded into Common Lisp.

This means: the code of the function runs after the arguments have been evaluated.

Using a macro

(combine-results (foo) (bar) (baz))

Since Lisp sees that it is a macro, it calls the macro at macro expansion time and generates code. What the generated code is, fully depends on the macro. What this allows us is to generate code like this:

(prepare-an-environment

  (embed-it (foo))
  (embed-it (bar))
  (embed-it (baz))

  (do-post-processing))

This code then will be executed. So for example you could set up system variables, provide error handlers, set up some report machinery, etc. Each individual form then also could be embedded into some other form. And after the functions run, one could do some clean up, reporting, etc. prepare-an-environment and embed-it would be macros or special operators, which make sure that some code runs before, around and after the embedded forms we provided.

We would have code executing before, around and after the provided forms. Is that useful? It might be. It might be useful for a more extensive test-framework.

If that sounds familiar, then you would see that one can get a similar code structure using CLOS methods (primary, before, after, around). The tests would run in a primary method and the other code would run as around, before and after methods.

Note that a macro could also print (see the comment by Hans23), inspect and/or alter the provided forms.