Can I have an indeterminate number of destructuring lists in a lisp macro lambda list?

83 Views Asked by At

I'm trying to write a macro that expands to an unspecified number of function calls, but I also want to be able to specify exactly one argument to be passed to each function in the macro call. I would basically want its syntax to look like a let call:

(let ((var1 1)
      (var2 2)
      ...
      (varn n))
  body)

but passing arguments to functions instead of binding variables, so a call like this

(foo ((fn1 arg1)
      (fn2 arg2)
      ...
      (fnn argn))
  body)

expands to this:

(progn
  (funcall fn1 arg1)
  (funcall fn2 arg2)
  ...
  (funcall fnn argn)
  body)

As far as I can tell, the list-destructuring behaviour of macro lambda lists allows me to pass an unspecified number of forms to the macro in a nested lambda list:

(defmacro foo ((&rest call-forms) &body body)
  ...)

OR define a rigid syntax for a form in a nested lambda list:

(defmacro foo ((single-fn single-arg) &body body)
  ...)

but NOT both:

(defmacro foo ((&rest (fn arg)) &body body) ; gibberish
  ...)

Is there a loophole or workaround I'm not seeing? I know it seems arbitrary, but the call syntax I specified above would be ideal for what I'm doing. I get that this might be out of the question, since let is a special operator and its behaviour appears to be unique, but I'd love to be proven wrong.

3

There are 3 best solutions below

4
Martin Půda On BEST ANSWER

Maybe you can completely drop lambda-list destructuring... I suggest this solution:

(defmacro funcall-macro (pairs &body body)
  (handler-case `(progn ,@(mapcar (lambda (pair)
                                    (destructuring-bind (fname arg) pair
                                      (list 'funcall fname arg)))
                                  pairs)
                   ,@body)
    (error (e) (error (format nil "~a~%" e)))))

With destructuring-bind, I check that each pair has exactly two elements.

Test:

(macroexpand-1 '(funcall-macro ((f1 a1)
                              (f2 a2)
                              (fn an))
                         (b1)
                         (b2)
                         (bn)))

(PROGN (FUNCALL F1 A1) (FUNCALL F2 A2) (FUNCALL FN AN) (B1) (B2) (BN))
T

This macro also works for pairs equal to ().

3
informatimago On

Unfortunately, no, you won't be able to specify that in the lambda-list. What is customary to do, instead of using (&rest clauses), (which would also allow for 0 clauses), is to specify the form of the first clause, and document that all the following should have the same form. So:

(defmacro foo (((fname argument) &rest other-fname-arguments) &body body)
  ;; check the form of the others-fname-argument
  (handler-case (every (lambda (fname-argument)
            (destructuring-bind (fname argument) fname-argument
              (declare (ignore fname argument))
              t))
                       other-fname-arguments)
    (error ()
      (error "The form of the other-fname-argument is not correct, it should be a list of (fname argument).")))
  (let ((fname-arguments (cons (list fname argument) other-fname-arguments)))
    `(progn
       ,@(mapcar (lambda (fname-argument)
                   (destructuring-bind (fname argument) fname-argument
                     (list 'funcall fname argument)))
                 fname-arguments)
       ,@body)))


(setf *print-right-margin* 30)

(pprint (macroexpand-1 '(foo ((f1 a1)
                              (f2 a2)
                              (fn an))
                         (b1)
                         (b2)
                         (bn))))

(progn
 (funcall f1 a1)
 (funcall f2 a2)
 (funcall fn an)
 (b1)
 (b2)
 (bn))
0
ignis volens On

One other interesting way to deal with this problem is to write a macro which checks one pair and then expands into a form which uses the same macro to check the rest. A natural way to do this is by pattern matching, and this example uses destructuring-match to do this, as it was specifically designed for processing macro forms in this way.

(defmacro foo (clauses &body forms)
  (destructuring-match clauses
    (()
     `(progn ,@forms))
    (((f a) &rest more)
     `(progn
        (funcall ,f ,a)
        (foo ,more ,@forms)))
    (otherwise
     (error "what even is this?"))))

Now (foo ((a 1) (b 2)) x) will expand into

(progn
  (funcall a 1)
  (progn
    (funcall b 2)
    (progn x)))

Which is equivalent to what you want. There's no need to worry about all the nested progns: the compiler will deal with those.

If (foo () ...) should not be legal, then you need something like this:

(defmacro foo (clauses &body forms)
  (destructuring-match clauses
    (((f a))
     `(progn
        (funcall ,f ,a)
        ,@forms))
    (((f a) &rest more)
     `(progn
        (funcall ,f ,a)
        (foo ,more ,@forms)))
    (otherwise
     (error "what even is this?"))))

You can also use destructuring-match as a more flexible but probably slower destructuring-bind. In particular you can implement one of the other solutions without the annoyance of having to catch the error if you want your own error message or condition:

(defmacro foo (clauses &body forms)
  `(progn
     ,@(mapcar (lambda (clause)
                 (destructuring-match clause
                   ((f a)
                    `(funcall ,f ,a))
                   (otherwise
                    (error "what even is this?"))))
               clauses)
     ,@forms))

Using either approach you can support more flexible syntax of course, so you could have something like what let does (although this would be hard to get right for this specific macro I think).