[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

How to write macro within macro



    Date: Fri, 22 Dec 89 12:45:37 MST
    From: intvax!gnome!drstrip@unmvax.cs.unm.edu (David R. Strip)

    I would like to write a macro that takes as input the name of a 
    flavor to be defined, as well as the names of the instance variables,
    and defflavors the flavor, and then creates a function whose name is
    the same as the flavor, and sets a variable to an instance of the
    flavor with all fields initialized by keywords.
    Example

    (def-thing foo a b c)
    creates
    (defflavor foo (a b c)() :initable-instance-variables (:contructor MAKE-FOO))
    (defmacro foo (id x y z) `(setq ,id (MAKE-FOO :a ,x :b ,y :c ,z)))

It's not the writing of the macro within a macro that I assume you
think is hard.  It's writing backquote within backquote.

You want something like the following:

(defmacro def-thing (name &rest ivs)
  (let ((maker (intern (format nil "MAKE-~A" name)))
	(id (make-symbol "ID")))
    `(progn (defflavor ,name ,ivs () 
	      :initable-instance-variables 
	      (:constructor ,maker))
	    (defmacro ,name (,id ,@ivs)
	      `(setq ,,id
		     (,',maker
		      ,@',(mapcan #'(lambda (x) 
				      (list (intern (string x) (find-package "KEYWORD"))
					    x))
				  ivs))))
	    ',name)))

In case it helps, here are some comments on how I think about these
things.  These are not the rules for how they are actually interpreted formally--
they are just some of the guidelines I personally use to help me navigate them
conceptually. I suspect that if you asked other backquote users, they'd give
you some similar but slightly different rules.  The important thing is not to
take these things as hard-and-fast ways you have to do them, but to help you
understand that people who use backquote a lot have macro-like idiomatic groupings
that they see as a unit--and to help you find some such groupings that you might
find useful for yourself...

 - think of every "'," you see in a string like ",',',','," as
   meaning "I just want to escape one layer out with no other interpretation.
   So in
   (eval (let ((x 1)) `(let ((x 2)) `(RESULT ,x ,',x)))) => (RESULT 2 1)
   the first x in the result list will escape out only 1 level
   and so will see the X that gets bound to 2, but the second X
   will escape a second level to see the outer binding.

 - backquotes match sort of like parens. that is, for any given set of commas
   and backquotes, the left-most backquote  matches the right-most comma.
    e.g., in   ``(,,x)
   the matching is 
	       `[1]  `[2]  ( ,[2]  ,[1]  x )
   but note that they are not like parens in that there can be more than
   one comma per backquote. so, something like the following
	       ``(,,x ,,y)
   has the following matchings:
	       `[1]  `[2]  ( ,[2] ,[1]  x   ,[2] ,[1] x )
   this is important because of evaluation order and evaluation environment
   issues.  the one that sees the outer binding will execute earlier
   (sometimes in a different environment, which is important for macros ).
   You can see this by doing:
   (eval (let ((x 1)) `(let ((x 2)) `(RESULT ,(print x) ,',(print x)))))
   1
   2 
   (RESULT 2 1)
   Note that since this is usually evaluated once in the macro contour and 
   once again at execution time, we have to force a call to EVAL for this 
   example to get the same number of evaluations.  Normally you wouldn't 
   write the  EVAL call explicitly.
   The same rule should help you understand triply nested ones, as in:
   (eval (eval (let ((x 1))
	   `(let ((x 2))
	      `(let ((x 3))
                 `(RESULT ,x ,',x ,',',x))))))
   (RESULT 3 2 1)
   Again, this would normally get used where two macro expansions and one
   final execution were going to occur, and again your code would not normally
   contain explicit calls to EVAL, but I needed to do them for example purposes.   

 - it might help (it does for me) to think of things like ,,@ and ,@,@
   like (MAPCAR "," ...) and (MAPCAR ",@" ...).  this description doesn't 
   make sense if you look at it too formally, but it does capture the sense.
   e.g.,
     (EVAL `(LET ((A 'AA) (B 'BB) (C 'CC) (D 'DD))
	      `(X ,,@(LIST '(LIST A B) '(LIST C D)))))
     => (X (AA BB) (CC DD))
   In English, the ,,@(LIST 'A 'B 'C 'D) might be thought of as mapping comma
   across the list which results from (LIST 'A 'B 'C 'D) so that the result
   is (,A ,B ,C ,D).
   Similarly,
     (EVAL `(LET ((A 'AA) (B 'BB) (C 'CC) (D 'DD))
	      `(X ,@,@(LIST '(LIST A B) '(LIST C D)))))
     => (X AA BB CC DD)
   because it sort of constructs
      (,@(LIST A B) ,@(LIST C D))

A footnote:

In some cases it's easier (and visually clearer) to avoid nested
backquotes and resort to using CONS, LIST, etc.  I didn't do that in my
reply to you above because you seemed bogged down in understanding how
backquote worked, and I wanted to help you understand that. But you
could instead have just done the following and avoided the double
backquote problem altogether:

(defmacro def-thing (name &rest ivs)
  (let ((maker (intern (format nil "MAKE-~A" name)))
	(id (make-symbol "ID")))
    `(progn (defflavor ,name ,ivs () 
	      :initable-instance-variables 
	      (:constructor ,maker))
	    (defmacro ,name (,id ,@ivs)
	      (list 'setq ,id
		    (list* ',maker
		           ,@(mapcan #'(lambda (x) 
					 (list (intern (string x) (find-package "KEYWORD"))
					       x))
				     ivs))))
	    ',name)))

It's worth remembering that there is no problem which -requires- the use
of backquote.  Backquote is intended as a convenient tool for you to use
when it seems helpful--not as a forced way of life to which you must
feel yourself a slave.  If the cost of figuring it out for some problem 
is higher than the cost of using some more conventional solution, you should
try to remember that the conventional solutions are another option you can
pursue.

This is somewhat hastily written, but I hope you find it helpful.