davypough
davypough

Reputation: 1941

Choosing/evaluating macro argument forms

The Common Lisp case macro always defaults to eql for testing whether its keyform matches one of the keys in its clauses. I'm aiming with the following macro to generalize case to use any supplied comparison function (although with evaluated keys):

(defmacro case-test (form test &rest clauses)
  (once-only (form test)
    `(cond ,@(mapcar #'(lambda (clause)
                         `((funcall ,test ,form ,(car clause))
                           ,@(cdr clause)))
            `,clauses))))

using

(defmacro once-only ((&rest names) &body body)
  "Ensures macro arguments only evaluate once and in order.
   Wrap around a backquoted macro expansion."
  (let ((gensyms (loop for nil in names collect (gensym))))
    `(let (,@(loop for g in gensyms collect `(,g (gensym))))
      `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
        ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
           ,@body)))))

For example:

(macroexpand '(case-test (list 3 4) #'equal
                ('(1 2) 'a 'b)
                ('(3 4) 'c 'd)))

gives

(LET ((#:G527 (LIST 3 4)) (#:G528 #'EQUAL))
  (COND ((FUNCALL #:G528 #:G527 '(1 2)) 'A 'B)
        ((FUNCALL #:G528 #:G527 '(3 4)) 'C 'D)))
  1. Is it necessary to worry about macro variable capture for a functional argument (like #'equal)? Can such arguments be left off the once-only list, or could there still be a potential conflict if #'equal were part of the keyform as well. Paul Graham in his book On Lisp, p.118, says some variable capture conflicts lead to "extremely subtle bugs", leading one to believe it might be better to (gensym) everything.

  2. Is it more flexible to pass in a test name (like equal) instead of a function object (like #'equal)? It looks like you could then put the name directly in function call position (instead of using funcall), and allow macros and special forms as well as functions?

  3. Could case-test instead be a function, instead of a macro?

Upvotes: 1

Views: 62

Answers (1)

sds
sds

Reputation: 60004

Variable capture

Yes, you need to put the function into the once-only because it can be created dynamically.

The extreme case would be:

(defun random-test ()
  (aref #(#'eq #'eql #'equal #'equalp) (random 4)))
(case-test foo (random-test)
  ...)

You want to make sure that the test is the same in the whole case-test form.

Name vs. object

Evaluating the test argument allows for very flexible forms like

(case-test foo (object-test foo)
  ...)

which allows "object-oriented" case-test.

Function vs. macro

Making case-test into a function is akin to making any other conditional (if and cond) into a function - how would you handle the proverbial

(case-test "a" #'string-equal
  ("A" (print "safe"))
  ("b" (launch missiles)))

Upvotes: 2

Related Questions