2
\$\begingroup\$

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?
asked Apr 13, 2012 at 3:19
\$\endgroup\$
7
  • \$\begingroup\$ Wow, that is remarkably similar (in concept, not in implementation) to my let-with macro. :-) \$\endgroup\$ Commented Apr 13, 2012 at 4:00
  • \$\begingroup\$ @ChrisJester-Young - Quite similar. The difference is that this is CL rather than Scheme (though I can't see that a lack of hygienic macros would kneecap me in this situation), and I have to cover one case other than a symbol key. Any idea for how I would effectively document this then, or are you of the mind that it's self-explanatory? \$\endgroup\$ Commented Apr 13, 2012 at 6:01
  • \$\begingroup\$ I prefer the let-alist style syntax, because it has similar syntactic structure to with-slots. Here, the alist is the object, and the keys are the slots. One addition I'd suggest: Make it so that (let-alist (sym1 sym2 sym3) alist ...) compiles. That is, if an element in the symbol/lookup pairs list isn't a list, use the same symbol for the let binding and the alist lookup. This would be the standard more common case I would think; then you can add the paired list if you want to make those two values different. Basically, give it a with-slots syntax and feel. \$\endgroup\$ Commented Apr 14, 2012 at 6:08
  • \$\begingroup\$ Oh, and another way to implement the binding is with macrolet. This would mean that the lookup is done separately every time the binding is found in body, and that may or may not be what you really want; just wanted to mention that this is another option. \$\endgroup\$ Commented Apr 14, 2012 at 6:12
  • \$\begingroup\$ And if you use macrolet instead of let, you may also get the ability to setf the binding for free (i.e., read and write on the binding). If not for free, you'll probably just have to write a single defsetf function. Generalized variables; macrolet; IMO all worthwhile things to look into. \$\endgroup\$ Commented Apr 14, 2012 at 6:44

2 Answers 2

2
\$\begingroup\$

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.

answered Apr 14, 2012 at 13:53
\$\endgroup\$
1
  • \$\begingroup\$ Good point. Revised. I was really only expecting to have to pass this thing raw alists, but it can't hurt to be prepared. What's your take on nested lets vs. a single let* in a situation like the above? \$\endgroup\$ Commented Apr 14, 2012 at 17:33
2
\$\begingroup\$

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.

answered Apr 14, 2012 at 18:02
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.