views:

228

answers:

5

Hi,

I've been writing some simple test cases for one of my assignments, and have built up a bit of a test suite using macros. I have run-test and run-test-section and so on. I'd like run-test-section to take a number of parameters which are run-test invocations and count up the number of PASSes and FAILs.

run-test returns T on PASS, and NIL on FAIL.

What I'm looking to do right now is write a macro that takes a &REST parameter, and invokes each of the elements of this list, finally returning the number of TRUE values.

This is what I currently have:

(defmacro count-true (&rest forms)
`(cond
    ((null ,forms)
      0)
    ((car ,forms)
      (1+ (count-true (cdr ,forms))))
    (T
      (count-true (cdr ,forms)))))

However this puts my REPL into an infinite loop. Might someone be able to point out how I can more effectively manipulate the arguments. Is this even a good idea? Is there a better approach?

edit:

As is noted in responses, a macro is not needed in this case. Using the inbuilt COUNT will suffice. There is useful information in the responses on recursive macro calls, however.

A: 

The problem is that your macro expands to an infinite form. The system will try to expand it all during the macro expansion phase and thus must run out memory.

Note that the test for the end of the forms list is never evaluated but expressed as code. It should be outside the backtick expression. And aas the other person explained, the (cdr forms) needs to be evaluated at macro expansion time as well, not left as code to be compiled.

I.e. something like this (untested):

(defmacro count-true (&rest forms)
  (if forms
      `(if (car ,forms)
           (1+ (count-true ,@(cdr forms)))
         (count-true ,@(cdr forms)))
    0))
HD
+3  A: 

At macro expansion time, cdr is not evaluated. So (count-true t t nil) hits an infinite expansion like this:

(count-true t t nil)
=>
(1+ (count-true (cdr (t t t nil))))
=>
(1+ (1+ (count-true (cdr (cdr (t t t nil))))))
=>
(1+ (1+ (1+ (count-true (cdr (cdr (cdr (t t t nil))))))))
=> ...

Well, actually this happens for both recursive branches at once. So it blows up even faster than the example.

A better idea?

  1. Trying writing the same thing as a function first. Figure out where you have to put the lambdas to delay evaluation. Then abstract the function into a macro so you can leave out the lambdas.

    The point of writing a function first, by the way, is that sometimes you'll find that a function is good enough. Writing a macro where a function would do is a mistake.

  2. Generally, when you write macros in Common Lisp, start with a loop rather than recursion. Recursive macros are tricky (and usually wrong :).

Edit:

Here is a more correct (but much longer) example:

(count-true t nil) =>
(cond
  ((null '(t nil)) 0)
  ((car '(t nil)) (1+ (count-true (cdr '(t nil)))))
  (T (count-true (cdr '(t nil)))))
=>
(cond
  ((null '(t nil)) 0)
  ((car '(t nil)) (1+ (1+ (count-true (cdr (cdr '(t nil)))))))
  (T (count-true (cdr (cdr '(t nil))))))
=>
(cond
  ((null '(t nil)) 0)
  ((car '(t nil)) (1+ (1+ (1+ (count-true (cdr (cdr (cdr '(t nil)))))))))
  (T (count-true (cdr (cdr (cdr '(t nil)))))))
=>
(cond
  ((null '(t nil)) 0)
  ((car '(t nil)) (1+ (1+ (1+ (1+ (count-true (cdr (cdr (cdr (cdr '(t nil)))))))))))
  (T (count-true (cdr (cdr (cdr (cdr '(t nil))))))))
Nathan Sanders
thanks for the explanation. i see now my faulty understanding of macro expansion. good tip on using `LOOP` instead. ive since figured out how to use just a function for this task.
Willi Ballenthin
+3  A: 

Forget recursive macros. They are a pain and really only for advanced Lisp users.

A simple, non-recursive version:

(defmacro count-true (&rest forms)
  `(+
    ,@(loop for form in forms
            collect `(if ,form 1 0))))

CL-USER 111 > (macroexpand '(count-true (plusp 3) (zerop 2)))
(+ (IF (PLUSP 3) 1 0) (IF (ZEROP 2) 1 0))

Well, here is a recursive macro:

(defmacro count-true (&rest forms)
  (if forms
      `(+ (if ,(first forms) 1 0)
          (count-true ,@(rest forms)))
    0))
Rainer Joswig
Why not `count ,form` instead of `+` and `collect`?
Svante
Because the LOOP does not count the results. It generates code. It is gone after expansion.
Rainer Joswig
Willi Ballenthin
+2  A: 

I believe that you are under two false impressions regarding what macros are.

Macros are written for expansion, functions for execution. If you write a recursive macro, it will recursively expand, without executing anything of the code it produces. A macro is not at all something like an inlined function!

When you write a macro, it can expand to function calls. When you write a macro, you do not venture into "macro land" where functions would be unavailable. It makes no sense at all to write count-true as a macro.

Svante
the idea of a recursive macro is usually that it expands into code, which uses that macro again, but on simpler/smaller arguments. The macro expansion halts when the macro is used in its simplest form. So the recursion is done via the macro expansion mechanism...
Rainer Joswig
thanks for getting my head straight. i figured out how to accomplish what i was looking for using just a function.i was under the impression i had to use a macro because of the way i thought i was interacting with evaluated expressions. now i see how i was mistaken.
Willi Ballenthin
A: 

As others have said, avoid recursive macros. If you're willing to do this in a function, you can use apply:

(defun count-true (&rest forms)
  (cond
    ((null forms) 0)
    (t (+ 1 (apply #'count-true (cdr forms))))))
JohnMaraist