Reputation: 1941
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)))
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.
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?
Could case-test
instead be a function, instead of a macro?
Upvotes: 1
Views: 62
Reputation: 60004
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.
Evaluating the test
argument allows for very flexible forms like
(case-test foo (object-test foo)
...)
which allows "object-oriented" case-test
.
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