Is there an advantage to this macro?
Asked Answered
G

2

7

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?

Grimona answered 27/1, 2016 at 5:23 Comment(2)
The real benefit of the macro is that it has access to the original forms and thus can print the form that failed to evaluate to a true value. It is sad that the book example does not actually show that.Unguinous
@Unguinous : in the book code the forms actually are macros, which print the result.Exo
K
6

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.

Keening answered 27/1, 2016 at 5:52 Comment(5)
The macro could be useful (for optimization, for example) if it short-circuited halfway through evaluation of its arguments, which the function approach never allows. But in this form it isn't.Dissenter
@JoaoTavora If the macro short-circuited, it would be a redundant implementation of the standard and macro!Keening
@Kaz, not quite since it would return t if all arguments are non-nil. But yes, basically. I think the real use in this case is the "verbosity" angle of @hans23: by using a macro you can print forms and do anything you want with the forms themselves.Dissenter
@JoaTavora; yes so it is equivalent to tacking one more T term into the arguments of AND. Boolean expressions which strictly return T for true have a virtue: they can be reliably compared with EQ. (defmacro bool-and (&rest args) `(and ,*args t)).Keening
@JoeTavora: I realized this more recently when I changed a library function called chr-isdigit in a Lisp dialect to return the digit value rather than t, when the character is a digit (making that function similar to the digit-char-p function in CL). However, this broke code such as [partition-by digit-char-p sequence] because now two consecutive digits that are different constitute different partitions, since the results are not equal.Keening
E
9

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.

Exo answered 27/1, 2016 at 10:3 Comment(0)
K
6

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.

Keening answered 27/1, 2016 at 5:52 Comment(5)
The macro could be useful (for optimization, for example) if it short-circuited halfway through evaluation of its arguments, which the function approach never allows. But in this form it isn't.Dissenter
@JoaoTavora If the macro short-circuited, it would be a redundant implementation of the standard and macro!Keening
@Kaz, not quite since it would return t if all arguments are non-nil. But yes, basically. I think the real use in this case is the "verbosity" angle of @hans23: by using a macro you can print forms and do anything you want with the forms themselves.Dissenter
@JoaTavora; yes so it is equivalent to tacking one more T term into the arguments of AND. Boolean expressions which strictly return T for true have a virtue: they can be reliably compared with EQ. (defmacro bool-and (&rest args) `(and ,*args t)).Keening
@JoeTavora: I realized this more recently when I changed a library function called chr-isdigit in a Lisp dialect to return the digit value rather than t, when the character is a digit (making that function similar to the digit-char-p function in CL). However, this broke code such as [partition-by digit-char-p sequence] because now two consecutive digits that are different constitute different partitions, since the results are not equal.Keening

© 2022 - 2024 — McMap. All rights reserved.