I heartily approve of the introduction of MAYBE-ONCE-ONLY in a5cf0df, because it drives me nuts (perhaps irrationally) to see superfluous bindings in generated code. If nothing else, they detract from PS's goal of producing readable JS.
Upgrading our codebase to use the latest Parenscript from git, I noticed this macro and saw an opportunity to remove a bit of code we wrote that does the same thing. Doing so, I noticed the following four things.
1. MAYBE-ONCE-ONLY wasn't being exported, so I modified package.lisp to do that. Should "ps" be in the name, in that case? MAYBE-ONCE-ONLY is a good name (it certainly kicks the name we were using's butt, as you will see below), but already a little on the verbose side, and MAYBE-PS-ONCE-ONLY is over my comfort limit in that respect. So I left the name as is.
2. There is a problem with symbol macros leading to multiple evaluation:
(defpsmacro true? (x) (maybe-once-only (x) `(and (!null ,x) (not (= ,x false)))))
(define-ps-symbol-macro aaa (bar))
(defun foo () (symbol-macrolet ((bbb (bar))) (when (true? bbb) (baz))) (when (true? aa) (baz)))
=> function foo() { if (bar() != null && bar() !== false) { baz(); }; return bar() != null && bar() !== false ? baz() : null; };
Note that this is true for both kinds of symbol macro — top-level and local — as the example shows.
3. The generated code evaluates bound expressions in reverse order of appearance in the code. I don't know if this would ever be a problem, but the principle of least surprise probably cautions against it. That is, in the following form, (bar) should probably be evaluated before (baz):
(defmacro+ps blah (a b) (maybe-once-only (a b) `(list ,a ,b)))
(defun foo () (blah (bar) (baz)))
=> function foo() { var b3600 = baz(); var a3599 = bar(); return [a3599, b3600]; };
4. Finally, here for comparison purposes is the macro I removed from our code in order to use MAYBE-ONCE-ONLY instead. It has a worse name, the horrible ONCE-WHEN (I couldn't come up with anything better), but I think I prefer its implementation for a couple of reasons: it doesn't suffer from the two problems (symbol macros and evaluation order) mentioned above, and it delegates to the classic ONCE-ONLY (PS-ONCE-ONLY in our case) for all of the macro magic, which makes it shorter and easier to follow.
(defun atomic? (expr) (or (atom expr) (atom (ps::ps-macroexpand expr))))
(defmacro once-when (vars &body body) (cond ((= (length vars) 1) `(cond ((atomic? ,(car vars)) ,@body) (t (ps-once-only (,(car vars)) ,@body)))) (t `(once-when (,(car vars)) (once-when ,(cdr vars) ,@body)))))
Perhaps the two implementations can be combined into something better.
Daniel
parenscript-devel@common-lisp.net