Take 4 (alteration based on feedback from Rainer Joswig):
(defmacro with-gensyms ((&rest names) &body body)
`(let ,(loop for n in names collect `(,n (gensym)))
,@body))
(defmacro let-alist ((&rest lookups) alist &body body)
(with-gensyms (alist-form)
`(let ((,alist-form ,alist))
(let ,(loop for l in lookups
if (keywordp l)
collect `(,(intern (symbol-name l)) (cdr (assoc ,l ,alist-form)))
else
collect `(,l (cdr (assoc ',l ,alist-form))))
,@body))))
This revision can still be used the same way
> (defparameter test '((|foo| . 1) (:bar . 2) (baz . 3)))
TEST
> (let-alist (:bar |foo| baz) test
(list |foo| bar baz))
(1 2 3)
but the macro only expands the passed-in alist
form once. The following is the output of slime-macroexpand-1
:
(LET ((#:G1299 TEST))
(LET ((BAR (CDR (ASSOC :BAR #:G1299)))
(|foo| (CDR (ASSOC '|foo| #:G1299)))
(BAZ (CDR (ASSOC 'BAZ #:G1299))))
(LIST |foo| BAR BAZ)))
All comments welcome, but specifically (in order of descending importance),
- Can you think of a good docstring?
- Can you think of a situation wherein this would explode?
- Is there a built-in macro that does the same thing, or close to it?
2 Answers 2
The main problem with it is alist
- the variable of the macro.
CL-USER > (pprint (macroexpand-1 '(let-alist (:bar |foo| baz)
(this-function-takes-a-long-time-or-has-side-effects)
(list |foo| bar baz))))
(LET ((BAR (CDR (ASSOC :BAR (THIS-FUNCTION-TAKES-A-LONG-TIME-OR-HAS-SIDE-EFFECTS))))
(|foo|
(CDR (ASSOC '|foo| (THIS-FUNCTION-TAKES-A-LONG-TIME-OR-HAS-SIDE-EFFECTS))))
(BAZ
(CDR (ASSOC 'BAZ (THIS-FUNCTION-TAKES-A-LONG-TIME-OR-HAS-SIDE-EFFECTS)))))
(LIST |foo| BAR BAZ))
As you can see the function is expanded multiple times into the code.
Rule 1: Always check the expansion for problems.
Rule 2: Do not expand the same code multiple times into the code.
Read 'On Lisp' by Paul Graham (free download available) for the various pitfalls of macros in Common Lisp and how to deal with those.
For above you need to generate a new unique variable and bind the value once.
-
\$\begingroup\$ Good point. Revised. I was really only expecting to have to pass this thing raw
alist
s, but it can't hurt to be prepared. What's your take on nestedlet
s vs. a singlelet*
in a situation like the above? \$\endgroup\$Inaimathi– Inaimathi2012年04月14日 17:33:32 +00:00Commented Apr 14, 2012 at 17:33
Here's an implementation that gives read and write access to the values in the alist.
To start, define a macro using symbol-macrolet, where a bound symbol in the body expands to code that accesses that symbol's value in the alist:
(defmacro with-alist% (alist-entries instance-form &body body)
`(symbol-macrolet
,(loop for (alist-binding alist-entry) in alist-entries
collect `(,alist-binding (cdr (assoc ',alist-entry ,instance-form))))
,@body))
And test it:
(defparameter *al* (list (cons 'foo 5) (cons 'bar 'a) (cons 'baz "z")))
*AL*
CL-USER>
(print *al*)
((FOO . 5) (BAR . A) (BAZ . "z"))
((FOO . 5) (BAR . A) (BAZ . "z"))
CL-USER>
(with-alist% ((foo foo) (bar bar)) *al*
(format t "foo=~a, bar=~a~%" foo bar)
(setf foo 1)
(setf bar "a") ...)
foo=5, bar=A
foo=1, bar=a
NIL
CL-USER>
(print *al*)
((FOO . 1) (BAR . "a") (BAZ . "z"))
((FOO . 1) (BAR . "a") (BAZ . "z"))
CL-USER>
Note that not only can you use the bound symbol for printing, you can also setf the symbol, which maps to changing the value associated with that symbol in the alist. This is what using symbol-macrolet (instead of let) provides. Also, if you read the source code for with-slots, at least for CCL, symbol-macrolet is how that macro is implemented. Again, I'm working towards a similar syntactic feel of with-slots.
with-alist% works well enough when you have an alist of mixed keywords and symbols, but I would think that most often you'll have just symbols, which means that there will be a lot of code duplication when using that function, for example:
(with-alist% ((foo foo) (bar bar)) *al*
Note how each alist entry is listed twice. What if you want the bound symbol and alist entry to always be the same? It would be nice if you could do this:
(with-alist (foo bar) *al*
And that macro:
(defmacro with-alist (alist-entries instance-form &body body)
`(with-alist% ,(mapcar (lambda (alist-entry)
(if (consp alist-entry)
`,alist-entry
`,(list alist-entry alist-entry)))
alist-entries)
,instance-form
,@body))
Usage:
(print *al*)
CL-USER>
((FOO . 1) (BAR . "a") (BAZ . "z"))
((FOO . 1) (BAR . "a") (BAZ . "z"))
CL-USER>
(with-alist (foo (bar% bar)) *al*
(format t "foo=~a, bar=~a~%" foo bar%)
(setf foo 5)
(setf bar% 'a) ...)
foo=1, bar=a
foo=5, bar=A
NIL
CL-USER>
(print *al*)
((FOO . 5) (BAR . A) (BAZ . "z"))
((FOO . 5) (BAR . A) (BAZ . "z"))
CL-USER>
REPL
I agree with Rainer's comment about only evaling instance-form once, when you only want read access. However, if you want read/write access, then instance-form will need to be expanded every time that a bound symbol is found in body. Otherwise, you'll be writing to an let binding of instance-form; not instance-form; and therefore instance-form won't be changed.
A good read for common-lisp macro programming is definitely On Lisp, and also Let Over Lambda. Getting through LOL is IME a serious time investment, but well worth it if you want to improve your common lisp macro programming skills.
let-with
macro. :-) \$\endgroup\$