From: Steve Long
Subject: Case Implementation
Date: 
Message-ID: <38069A78.B5EDE69D@isomedia.com>
OK, I've read the Graham book (On Lisp) on advanced Lisp and even found
an error in one of the examples. Nevertheless, some macros escape me,
and it's often a crap shoot to get a large one working. With this in
mind, could anyone show me how you might implement the CASE macro from
scratch?

slong

From: Pierre R. Mai
Subject: Re: Case Implementation
Date: 
Message-ID: <87d7ugd99r.fsf@orion.dent.isdn.cs.tu-berlin.de>
Steve Long <·········@isomedia.com> writes:

> OK, I've read the Graham book (On Lisp) on advanced Lisp and even found
> an error in one of the examples. Nevertheless, some macros escape me,
> and it's often a crap shoot to get a large one working. With this in
> mind, could anyone show me how you might implement the CASE macro from
> scratch?

Here is an implementation I just wrote from scratch, which by accident 
produces output very close to the way CMU CL expands case (which isn't
very surprising, given that this is the straight forward way of
implementing case):

(defmacro mycase (keyform &body clauses)
  (loop with key-sym = (gensym)
	for (keys . forms) in clauses
	collect
	(if (or (eql keys 't) (eql keys 'otherwise))
	    ;; The otherwise clause
	    `(t ,@forms)
	    ;; A normal clause
	    (let ((key-list (if (listp keys) keys (list keys))))
	      `((or ,@(mapcar #'(lambda (key) `(eql ,key-sym (quote ,key)))
			      key-list))
		,@forms)))
	into cond-clauses
	finally
	(return
	  `(let ((,key-sym ,keyform))
	     (cond
	       ,@cond-clauses)))))

I find that I often use loop with collect-into and finally clauses
in conjunction with backquote to produce expansions of macros with
clauses.  Others would probably use an iteration construct, like
iterate/collect.

The above macro does little to no error checking on expansion, which
an industrial strength case would do, see the CMU CL sources for an
implementation of case that is industrial strength in this regard.

Regs, Pierre.

-- 
Pierre Mai <····@acm.org>         PGP and GPG keys at your nearest Keyserver
  "One smaller motivation which, in part, stems from altruism is Microsoft-
   bashing." [Microsoft memo, see http://www.opensource.org/halloween1.html]
From: Erann Gat
Subject: Re: Case Implementation
Date: 
Message-ID: <gat-1510991012270001@milo.jpl.nasa.gov>
In article <·················@isomedia.com>, ·········@isomedia.com wrote:

> OK, I've read the Graham book (On Lisp) on advanced Lisp and even found
> an error in one of the examples. Nevertheless, some macros escape me,
> and it's often a crap shoot to get a large one working. With this in
> mind, could anyone show me how you might implement the CASE macro from
> scratch?

Start with a crude first cut:

  (defmacro mycase (expression &rest clauses)
    `(let ( (value ,expression) )
       (cond
        ,@(mapcar 'translate-case-clause-into-cond-clause clauses))))

Let's assume for the moment that only single keys are allowed in case clauses.
Then translating a case clause into a cond clause looks like:

  (defun translate-case-clause-into-cond-clause (clause)
    `((eq value (quote ,(car clause))) ,@(cdr clause)))

I've spelled out the QUOTE special form to make it easier to understand
what's going on.  In practice you'd write ',(car clause).

If we want to extend this to handle non-atomic keys then we need
something like:

  (defun translate-case-clause-into-cond-clause (clause)
    `(,(translate-case-key (car clause)) ,@(cdr clause)))

  (defun translate-case-key (key)
    (if (atom key)
      `(eq value ',key)
      `(member value ',key)))

You can roll all this up into a single macro without the helper
functions:

  (defmacro mycase (expression &rest clauses)
    `(let ( (value ,expression) )
       (cond
        ,@(mapcar (lambda (clause)  ; translate-case-clause-into-cond-clause
                    `(,(if (atom (car clause))  ; translate-case-key inlined
                         `(eq value ',(car clause))
                         `(mamber value ',(car clause)))
                      ,@(cdr clause)))
                  clauses))))

There are two bugs in this version.  First, it doesn't handle OTHERWISE.
(I'll leave that as an excercise for the reader.)  Second, and more
serious, suppose you do:

  (defun my-function (x)
    (let ( (value (+ x x)) )
      (mycase x
        (2 value))))

You'd expect my-function to return 4 when x is 2, but in fact it returns 2.
This is because the symbol VALUE that you are using as a temporary is
shadowed by the same symbol used by the MYCASE macro.  There are two
ways to get around this problem.  One is to choose an obscure name instead
of VALUE for the MYCASE macro.  The second is to generate a variable name
on the fly:

  (defmacro mycase (expression &rest clauses)
    (let ( (value-variable (gensym "VALUE")) )
      `(let ( (,value-variable ,expression) )
         (cond ...
               ; Rest as before, but with all occurrences of VALUE
               ; replaced with ,VALUE-VARIABLE.
               ))))

This leads to more complicated macro code, but it guarantees that no
obscure bugs will arise because of accidentally conflicting variable
names.  It's generally considered the preferred solution.

Erann Gat
···@jpl.nasa.gov
From: Howard R. Stearns
Subject: Re: Case Implementation
Date: 
Message-ID: <380CC1C4.80C7BAE2@elwood.com>
One language lawyer point, not really important for all but
implementors/documentors:
 
The specifications for CASE and TYPECASE both say:

  Each of the normal-clauses is then considered in turn. If the
  test-key [matches the key for that clause], the forms in that
  clause are evaluated as an implicit progn, and the values it returns
  are returned as the value of the [entire] form. 

Since (PROGN) => NIL, I would assume that when a clause with no forms
is selected, NIL is returned from the entire CASE/TYPECASE form.  I
believe this interpretation is consistent with current practice, and
what users expect.


However, the specification for COND is slightly different:

  Test-forms are evaluated one at a time in the order in which they
  are given in the argument list until a test-form is found that
  evaluates to true.  

  If there are no forms in that clause, the primary value of the
  test-form is returned by the cond form.  Otherwise, the forms
  associated with this test-form are evaluated in order, left to
  right, as an implicit progn, and the values returned by the last
  form are returned by the cond form.  

This allows COND to be used like a more flexible OR, in which some
clauses, when true can be made to return not just the non-null value,
but a different value.

If CASE is implemented in terms of COND, then in a production system, 
one should probably check for an empty set of value forms in each CASE 
clause, and explicitly return NIL from the generated COND clause.

Thomas A. Russ wrote:
> 
> ;;; General procedure:
> ;;;   (1)  Figure out what code you want the macro to produce.
> ;;;        For CASE, I choose to implement using COND
> ;;;   (2)  For each of the variants of the CASE tags, decide
> ;;;        who to implement it.
> ;;;        Symbol => EQL test
> ;;;        List   => MEMBER test
> ;;;        otherwise => Handled specially, goes to T
> 
> USER>
> (defmacro my-case (datum &body cases)
>   ;; Case Macro.  Iterates through the cases and builds
>   ;;  up a COND form to evaluate the test clauses.  Returns
>   ;;  such a cond.
>   ;; Note that the test datum is bound to
>   ;;  a GENSYM'd variable to avoid multiple evaluation.
>   ;; Note also that the backquotes introduce quotes.
>   (let ((cond-clauses nil)
>         (datum-variable (gensym "CASE-DATUM")))
> 
>     (dolist (case cases)
>         ;; I don't recall offhand whether T is a synonym for otherwise in
>         ;;  CASE statements.  If it is, you will need another clause
>         ;;  like the following to handle it as well.
> 
>             ;; Handle special symbol(s):
>       (cond ((eq (first case) 'otherwise)
>              (push `(t ,@(rest case))
>                    cond-clauses))
> 
>             ;; List => Member
>             ((listp (first case))
>              (push `((member ,datum-variable ',(first case))
>                      ,@(rest case))
>                    cond-clauses))
> 
>             ;; Everything else goes to EQL
>             (t (push `((eql ,datum-variable ',(first case))
>                        ,@(rest case))
>                      cond-clauses))))
> 
>     `(let ((,datum-variable ,datum))  ;; Evaluate Once
>        ;; We built the result using PUSH, so it is backwards:
>        (cond ,@(nreverse cond-clauses))) ))
> 
> USER> MY-CASE
> USER> USER> (my-case 'foo
>                (bar 'ick)
>                (baz (print "foo - hah hah"))
>                ((foo jump) 'goody)
>                (otherwise 'sol))
> GOODY
> 
> ;; This is why we need the LET and GENSYM:
> USER> (defvar i 3)
> I
> USER> (my-case (incf i)
>                (bar 'ick)
>                (baz (print "foo - hah hah"))
>                ((foo jump) 'goody)
>                ((1 3 5 9) 'nope)
>                ((2 4 6 8) 'appreciate)
>                (otherwise 'sol))
> APPRECIATE
> USER> i
> 4
> 
> ;; Macroexpand is a good tool to use when trying to debug
> ;;   macros that you are writing.  It shows you what a macro
> ;;   invocation will look like.
> 
> USER> (macroexpand '(my-case (incf i)
>                (bar 'ick)
>                (baz (print "foo - hah hah"))
>                ((foo jump) 'goody)
>                ((1 3 5 9) 'nope)
>                ((2 4 6 8) 'appreciate)
>                (otherwise 'sol)))
> (LET ((#:CASE-DATUM53 (INCF I)))
>   (COND ((EQL #:CASE-DATUM53 'BAR) 'ICK)
>         ((EQL #:CASE-DATUM53 'BAZ) (PRINT "foo - hah hah"))
>         ((MEMBER #:CASE-DATUM53 '(FOO JUMP)) 'GOODY)
>         ((MEMBER #:CASE-DATUM53 '(1 3 5 9)) 'NOPE)
>         ((MEMBER #:CASE-DATUM53 '(2 4 6 8)) 'APPRECIATE)
>         (T 'SOL)))
> T
> USER>
> 
> --
> Thomas A. Russ,  USC/Information Sciences Institute          ···@isi.edu
From: Pierre R. Mai
Subject: Re: Case Implementation
Date: 
Message-ID: <87puyb14y3.fsf@orion.dent.isdn.cs.tu-berlin.de>
"Howard R. Stearns" <······@elwood.com> writes:

> One language lawyer point, not really important for all but
> implementors/documentors:
>  
> The specifications for CASE and TYPECASE both say:
> 
>   Each of the normal-clauses is then considered in turn. If the
>   test-key [matches the key for that clause], the forms in that
>   clause are evaluated as an implicit progn, and the values it returns
>   are returned as the value of the [entire] form. 
> 
> Since (PROGN) => NIL, I would assume that when a clause with no forms
> is selected, NIL is returned from the entire CASE/TYPECASE form.  I
> believe this interpretation is consistent with current practice, and
> what users expect.
> 
> 
> However, the specification for COND is slightly different:
> 
>   Test-forms are evaluated one at a time in the order in which they
>   are given in the argument list until a test-form is found that
>   evaluates to true.  
> 
>   If there are no forms in that clause, the primary value of the
>   test-form is returned by the cond form.  Otherwise, the forms
>   associated with this test-form are evaluated in order, left to
>   right, as an implicit progn, and the values returned by the last
>   form are returned by the cond form.  
> 
> This allows COND to be used like a more flexible OR, in which some
> clauses, when true can be made to return not just the non-null value,
> but a different value.
> 
> If CASE is implemented in terms of COND, then in a production system, 
> one should probably check for an empty set of value forms in each CASE 
> clause, and explicitly return NIL from the generated COND clause.

Thanks for bringing this point up.  At the time I was to lazy to look
up and compare the specs on COND and CASE, and was therefore wondering 
why CMUCL introduced "superfluous" NILs into the expansion of CASE.
Now I see why they are introduced...

Regs, Pierre.

-- 
Pierre Mai <····@acm.org>         PGP and GPG keys at your nearest Keyserver
  "One smaller motivation which, in part, stems from altruism is Microsoft-
   bashing." [Microsoft memo, see http://www.opensource.org/halloween1.html]
From: Rob Warnock
Subject: Re: Case Implementation
Date: 
Message-ID: <7uja9c$5g6mm@fido.engr.sgi.com>
Howard R. Stearns <······@elwood.com> wrote:
+---------------
| The specifications for CASE and TYPECASE both say:
|   Each of the normal-clauses is then considered in turn. If the
|   test-key [matches the key for that clause], the forms in that
|   clause are evaluated as an implicit progn, and the values it returns
|   are returned as the value of the [entire] form. 
| 
| Since (PROGN) => NIL, I would assume that when a clause with no forms
| is selected, NIL is returned from the entire CASE/TYPECASE form.  I
| believe this interpretation is consistent with current practice, and
| what users expect.
+---------------

Interesting... It's certainly what CMUCL returns:

	* (case 3 (4 'foo) (3) (t 'bar))
	NIL
	* 

but not CLISP!!

	> (case 3 (4 'foo) (3) (t 'bar))
	T
	> 

[Well, CLISP "1996-04-17 (April 1996)". Maybe fixed by now...?]


-Rob

-----
Rob Warnock, 8L-846		····@sgi.com
Applied Networking		http://reality.sgi.com/rpw3/
Silicon Graphics, Inc.		Phone: 650-933-1673
1600 Amphitheatre Pkwy.		FAX: 650-933-0511
Mountain View, CA  94043	PP-ASEL-IA
From: Barry Margolin
Subject: Re: Case Implementation
Date: 
Message-ID: <7nlP3.111$UJ6.4315@burlma1-snr2>
In article <·················@elwood.com>,
Howard R. Stearns <······@elwood.com> wrote:
>If CASE is implemented in terms of COND, then in a production system, 
>one should probably check for an empty set of value forms in each CASE 
>clause, and explicitly return NIL from the generated COND clause.

Actually, it's even simpler than that.  Rather than make a special check,
just make each COND clause include an explicit PROGN, rather than depending
on COND's implicit PROGN, i.e. in all the places where the macro has:

	,@(rest case)

change it to:

	(progn ,@(rest case))

-- 
Barry Margolin, ······@bbnplanet.com
GTE Internetworking, Powered by BBN, Burlington, MA
*** DON'T SEND TECHNICAL QUESTIONS DIRECTLY TO ME, post them to newsgroups.
Please DON'T copy followups to me -- I'll assume it wasn't posted to the group.
From: Pierre R. Mai
Subject: Re: Case Implementation
Date: 
Message-ID: <873dv62cwb.fsf@orion.dent.isdn.cs.tu-berlin.de>
Barry Margolin <······@bbnplanet.com> writes:

> In article <·················@elwood.com>,
> Howard R. Stearns <······@elwood.com> wrote:
> >If CASE is implemented in terms of COND, then in a production system, 
> >one should probably check for an empty set of value forms in each CASE 
> >clause, and explicitly return NIL from the generated COND clause.
> 
> Actually, it's even simpler than that.  Rather than make a special check,
> just make each COND clause include an explicit PROGN, rather than depending
> on COND's implicit PROGN, i.e. in all the places where the macro has:
> 
> 	,@(rest case)
> 
> change it to:
> 
> 	(progn ,@(rest case))

Or change `(,test ,@(rest case)) to `(,test nil ,@(rest case)) making 
the result behaviour more explicit...

Regs, Pierre.

-- 
Pierre Mai <····@acm.org>         PGP and GPG keys at your nearest Keyserver
  "One smaller motivation which, in part, stems from altruism is Microsoft-
   bashing." [Microsoft memo, see http://www.opensource.org/halloween1.html]