Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

How can i make "case" to use equal?

707 views
Skip to first unread message

Kalamaro

unread,
Sep 4, 2001, 8:08:19 AM9/4/01
to
"case", as well as "member", uses eql to compare the forms. When you use
"member", you can use "equal" by putting

(member '(this) these : test #'equal)

I'm trying to do the same in "case" clauses. I've tryed putting ": test
#'equal" in several places of the "case" clause but it didn't work.

how can i do it?

Kent M Pitman

unread,
Sep 4, 2001, 9:45:42 AM9/4/01
to
"Kalamaro" <fe...@usuarios.retecal.es> writes:

>
> "case", as well as "member", uses eql to compare the forms. When you use
> "member", you can use "equal" by putting
>
> (member '(this) these : test #'equal)

^
Do not put a space here.
It may work in some implementations, but not in all.



> I'm trying to do the same in "case" clauses. I've tryed putting ": test
> #'equal" in several places of the "case" clause but it didn't work.
>
> how can i do it?

":test" is one token. There must be no space between the ":" and the "test".

:test is a keyword. Keywords in Lisp have a prefix colon followed directly
by the symbol name, a name in the KEYWORD package. It's the same as a
symbol like CL:CAR or KEYWORD:TEST. It *is* a symbol in the KEYWORD package,
in fact, so you could actually say KEYWORD:TEST there, but :TEST is the
accepted way to write that. NO ONE wites out KEYWORD:xxx when denoting
keywords; it's bad style.

Kent M Pitman

unread,
Sep 4, 2001, 10:00:50 AM9/4/01
to
"Kalamaro" <fe...@usuarios.retecal.es> writes:

OOPS. I misread your question. Lo siento. Let me try again...

CASE doesn't have a variant that uses EQUAL. You'd have to write it
yourself.

e.g., you want something like:

(defmacro case-equal (exp &body clauses)
(let ((temp (gensym)))
`(let ((,temp ,exp))
(cond ,@(mapcar #'(lambda (clause)
(destructuring-bind (keys . clause-forms) clause
(cond ((eq keys 'otherwise)
`(t ,@clause-forms))
(t
(if (atom keys) (setq keys (list keys)))
`((member ,temp ',keys :test #'equal)
,@clause-forms)))))
clauses)))))

This defines a CASE-EQUAL macro like the system's CASE macro but that
uses EQUAL. You can use Lisp to see the expansion if you like:

(pprint (macroexpand-1 '(case-equal '(a b)
(((a b) (c d)) 3)
(otherwise 4))))

(LET ((#:G5219 '(A B)))
(COND ((MEMBER #:G5219 '((A B) (C D)) :TEST #'EQUAL)
3)
(T 4)))

If you want to supply an arbitrary predicate, you'll need to make the
macro more general. e.g., the following should work, though I didn't
take time to test it:

(defmacro case-using (pred-exp exp &body clauses)
(let ((temp (gensym)) (pred (gensym "PRED")))
`(let ((,pred ,pred-exp) (,temp ,exp))
(cond ,@(mapcar #'(lambda (clause)
(destructuring-bind (keys . clause-forms) clause
(cond ((eq keys 'otherwise)
`(t ,@clause-forms))
(t
(if (atom keys) (setq keys (list keys)))
`((member ,temp ',keys :test ,pred)
,@clause-forms)))))
clauses)))))

(case-using #'equal '(a b)
(((a b) (c d)) 3)
(otherwise 4))
=> 3

Erik Naggum

unread,
Sep 4, 2001, 11:43:06 AM9/4/01
to
* "Kalamaro" <fe...@usuarios.retecal.es>

Nothing beats reading the specification to find out how such things work.
A copy of the standard or something very much like it almost certainly
comes with your Common Lisp implementation or it provides pointers to
resources on the Net. I would suggest you look for the documentation
available with your Common Lisp system.

However, I trust that you will look for the documentatio, so this sketchy
answer will be valuable in context of what you find in the documentation.
You are quite right that case, ccase and ecase all use eql for the test
and you cannot change this. This is because the cases are specified as
literals in the source of your program, quite unlike the elements of a
sequence searched by member and the like at run-time. It is expected
that the compiler make good use of the fact that these are literals. You
find the same "restriction" in all other language that have a case-like
branching form. However, it would be in the Common Lisp spirit to make a
more general form available without cost to the programmer, although it
would be a little more expensive to implement. The key to implement a
more general form is that we should keep the very valuable optimization
quality of testing for identity. There are several ways to accomplish
this, but I prefer the following:

(defun extract-case-keys (case-form)
(loop for clause in (cddr case-form)
until (and (eq (car case-form) 'case) (member (car clause) '(t otherwise)))
if (listp (car clause))
append (car clause)
else
collect (car clause)))

;; export if packaged
(defparameter *with-hashed-identity-body-forms*
'((case . extract-case-keys)
(ccase . extract-case-keys)
(ecase . extract-case-keys))
"Alist of the valid operators in body forms of a with-hashed-identity form
with their key-extraction function.")

(defun with-hashed-identity-error (body)
(error "Body form of with-hashed-identity is ~A, but must be one of:~{ ~A~}."
(caar body) (mapcar #'car *with-hashed-identity-body-forms*)))

(defun with-hashed-identity-hashtable (hash-table body)
(dolist (key (funcall (or (cdr (assoc (car body) *with-hashed-identity-body-forms*))
'with-hashed-identity-error)
body))
(setf (gethash key hash-table) key))
hash-table)

;; export if packaged
(defmacro with-hashed-identity (hash-options &body body)
"A wrapper around case forms to enable case tests via a hashtable."
(unless (and (listp (car body))
(null (cdr body))) ;TODO: Allow multiple body forms.
(error "Body of with-hashed-identity must be a single form.")
(let ((hash-table (make-symbol "hashtable")))
`(let ((,hash-table (load-time-value
(with-hashed-identity-hashtable (make-hash-table ,@hash-options)
',(car body)))))
(,(caar body) (gethash ,(cadar body) ,hash-table) ,@(cddar body)))))

This allows the following forms to succeed:

(with-hashed-identity (:test #'equal)
(case "foo"
("foo" 'yeah)
(t 'bummer)))

(with-hashed-identity (:test #'equalp)
(case "foo"
("FOO" 'yeah)
(t 'bummer)))

(with-hashed-identity (:test #'equalp)
(case (vector #\f #\o #\o)
(#(#\F #\O #\O) 'yeah)
(t 'bummer)))

Style hint: The keyword is written :test. An intervening space should
signal a reader error, whether the Common Lisp reader or a human reader.

By the way, this is the kind of macros I write when I want to enhance the
language to do more than it is specified to do. The difference between
my style and that of those with, say, 23 years of experience may be
because I studied Common Lisp when it was being standardized and for all
practical purposese had reached its "final form", as opposed to learning
"Lisp" during the horrible mess that preceded all standardization efforts
two decades earlier. Sometimes, it is very beneficial to start studying
something only in its mature state. For instance, the Perl code written
by people who start with Perl 5.6 is vastly superior to that of those who
started with Perl 4 or even earlier versions. Similarly, those who start
with Java or C++ now are probably going to write vastly superior code to
those who have been tagged along on its evolutionary path. I think my
luck in timing is that I have arrived on the scene when most of the work
has been done to build the infrastructure and I have been able to build
on top of a solid foundation I could trust, as opposed to having lived
with a "fluidity" that caused trusting the foundation to be very risky,
indeed. However, more than half a decade has passed since Common Lisp
solidified, so those who once could not trust the standard should have
had ample time to adjust by now.

///

Espen Vestre

unread,
Sep 6, 2001, 5:22:13 AM9/6/01
to
The need for an "equal case" has struck me several times, are there anybody
here who regularily uses a macro for this? A series of if-then-else tests for
equality of strings is pretty common, and I think that a CASE version of
such tests is much more readable than a COND (or IF*!) version.
--
(espen)

Sam Steingold

unread,
Sep 6, 2001, 12:18:14 PM9/6/01
to
> * In message <w6n148h...@wallace.ws.nextra.no>
> * On the subject of "Re: How can i make "case" to use equal?"
> * Sent on 06 Sep 2001 11:22:13 +0200

(defun case-expand (form-name test keyform clauses)
(let ((var (gensym (format nil "~a-KEY-" form-name))))
`(let ((,var ,keyform))
(cond
,@(maplist
#'(lambda (remaining-clauses)
(let ((clause (first remaining-clauses))
(remaining-clauses (rest remaining-clauses)))
(unless (consp clause)
(error "~S: missing key list" form-name))
(let ((keys (first clause)))
`(,(cond ((or (eq keys 't) (eq keys 'otherwise))
(if remaining-clauses
(error "~S: the ~S clause must be the last one"
form-name keys)
't))
((listp keys)
`(and ,@(mapcar #'(lambda (key)
`(,test ,var ',key))
keys)))
(t `(,test ,var ',keys)))
,@(rest clause)))))
clauses)))))

(defmacro fcase (test keyform &body clauses)
(case-expand 'fcase test keyform clauses))

> (macroexpand '(fcase equalp foo ("bar" 1) (("baz" "zot") 2) (otherwise 3)))

(let ((#:FCASE-KEY-1452 foo))
(cond ((equalp #:FCASE-KEY-1452 '"bar") 1)
((and (equalp #:FCASE-KEY-1452 '"baz") (equalp #:FCASE-KEY-1452 '"zot")) 2)
(t 3))) ;
t

--
Sam Steingold (http://www.podval.org/~sds)
Support Israel's right to defend herself! <http://www.i-charity.com/go/israel>
Read what the Arab leaders say to their people on <http://www.memri.org/>
I want Tamagochi! -- What for? Your pet hamster is still alive!

Sam Steingold

unread,
Sep 6, 2001, 12:41:37 PM9/6/01
to
> * In message <w6n148h...@wallace.ws.nextra.no>
> * On the subject of "Re: How can i make "case" to use equal?"
> * Sent on 06 Sep 2001 11:22:13 +0200
> * Honorable Espen Vestre <espen@*do-not-spam-me*.vestre.net> writes:
>

(defun case-expand (form-name test keyform clauses)


(let ((var (gensym (format nil "~a-KEY-" form-name))))
`(let ((,var ,keyform))
(cond
,@(maplist
#'(lambda (remaining-clauses)
(let ((clause (first remaining-clauses))
(remaining-clauses (rest remaining-clauses)))
(unless (consp clause)
(error "~S: missing key list" form-name))
(let ((keys (first clause)))
`(,(cond ((or (eq keys 't) (eq keys 'otherwise))
(if remaining-clauses
(error "~S: the ~S clause must be the last one"
form-name keys)
't))
((listp keys)

`(or ,@(mapcar #'(lambda (key)


`(,test ,var ',key))
keys)))
(t `(,test ,var ',keys)))
,@(rest clause)))))
clauses)))))

(defmacro fcase (test keyform &body clauses)
(case-expand 'fcase test keyform clauses))

> (macroexpand '(fcase equalp foo ("bar" 1) (("baz" "zot") 2) (otherwise 3)))

(let ((#:FCASE-KEY-1452 foo))
(cond ((equalp #:FCASE-KEY-1452 '"bar") 1)

((or (equalp #:FCASE-KEY-1452 '"baz") (equalp #:FCASE-KEY-1452 '"zot")) 2)
(t 3))) ;
t

[cancelled and re-posted due to a typo in code]

--
Sam Steingold (http://www.podval.org/~sds)
Support Israel's right to defend herself! <http://www.i-charity.com/go/israel>
Read what the Arab leaders say to their people on <http://www.memri.org/>

In C you can make mistakes, while in C++ you can also inherit them!

Erik Naggum

unread,
Sep 6, 2001, 2:32:43 PM9/6/01
to
* Sam Steingold <s...@gnu.org>

> (defun case-expand (form-name test keyform clauses)
> ...)

>
> (defmacro fcase (test keyform &body clauses)
> (case-expand 'fcase test keyform clauses))

Neat, but one of the really good things about case and the like is that
they can be optimized into jump tables and the like since the keys are
all constants at compile-time. Expanding into cond is not a good idea if
you want to keep this. In fact, using a hash table for the keys allows
you to convert the keys into small integers.

///

Thomas F. Burdick

unread,
Sep 6, 2001, 3:36:46 PM9/6/01
to
Erik Naggum <er...@naggum.net> writes:

The one thing I would have done differently from your version would be
to special-case EQL to make it expand into a normal CASE form. I'm
guessing that would give better performance for things like
(case x
(0 ...)
(1 ...)
(2 ...)
...
(15 ...))
where you've already told the compiler that X is [0, 15].

Kent M Pitman

unread,
Sep 6, 2001, 4:05:47 PM9/6/01
to

I would leave it to the compiler to know what can be special-cased and
whatcan't. It might be that things can be optimized for reasons invisible
to you. For example, the compiler might have special knowledge that a pointer
will not change, and might use binary search on the literal pointer to
select among the elements quickly. (In some cases it might get help from the
loader.) (Or, at least, it's allowed to do this. All you know is that you're
not allowed to know.)

The reason we have standard idioms is so that compiler writers can spend a lot
of time trying to figure out how to hyper-optimize them. The more we turn
these into low-level equivalents (e.g., writing our own LOOP because we don't
like the system one) the more we thwart the ability of the system to do things
we can't know about that make things better.

Erik Naggum

unread,
Sep 6, 2001, 4:16:27 PM9/6/01
to
* Thomas F. Burdick

> The one thing I would have done differently from your version would be
> to special-case EQL to make it expand into a normal CASE form.

It would be a wasted gethash call, but otherwise no different at all.
So I am not sure what you think would be so different.

> I'm guessing that would give better performance for things like
> (case x
> (0 ...)
> (1 ...)
> (2 ...)
> ...
> (15 ...))
> where you've already told the compiler that X is [0, 15].

Nothing would happen to these keys, and the case would do exactly the
same as without my code.

If you did not specifically refer to my code but replied to my article,
anyway, ignore this. Replying to a slightly different article than you
appear to is normal on USENET, but it is still a little confusing.

///

0 new messages