Search code examples
functionmacroslispcommon-lispsbcl

Invoking Common Lisp macros systematically with varying expressions


I am learning Common Lisp (SBCL).
I want to create a facility to invoke two (or more) macros with several similar expressions that differ only in some parameters.

I would like to define the base of the expression, then modify it with the parameters I supply. For this a lambda function definition came to mind.

As far as I know, there is no analogue to funcall for macros, so I've also wrapped the macros in lambdas.

I feel like I'm overcomplicating with with all these lambda-s and funcall-s. Is there a more elegant way?

The macros are from an external lib, so I'd prefer not to modify them.
(Specifically, the fiveam testing library's finishes and signals.)

Here is a sample code:

(defmacro macro1 (body) ())
(defmacro macro2 (body) ())

(defun check-expr-with-args (do-m func args)
  (dolist (arg args)
    (format t "~a " arg)
    (funcall do-m (lambda () (funcall func arg)))))

(let ((test-expr 
        #'(lambda (val) (format t "~a" val)))
      (cases (list
               (list #'(lambda (func) ( macro1 (funcall func))) 
                     (list 1 2 3 4 5))
               (list #'(lambda (func) ( macro2 (funcall func)))
                     (list -4 -5 -6 -7 -8 -9)))))
  (dolist (c cases)
    (check-expr-with-args (first c) test-expr (second c))))

Originally I've tried to pass the macro names to my check-expr-with-args function, and the expressions in quoted form, relying on lexical scoping for the parameter insertion. That didn't work out.


Solution

  • I think you can write a wrapper macro that produces code that invokes macro1 (and macro2). For example here I'm defining m1 that takes (i) a test expression and (ii) an expression that is expected to evaluates at runtime to a list of values.

    (defmacro m1 (test-expr input-expr)
      (let ((arg (gensym)))
        `(dolist (,arg ,input-expr)
           (macro1 ,test-expr ,arg))))
    

    Both test-expr and input-expr are injected in a dolist expression, which binds a variable named arg. Here arg is a fresh symbol introduced with gensym, to avoid accidentally shadowing a variable or symbol-macro possibly used in test-expr.

    For example:

    (m1 (some-test-p) (list 1 2 3 4))
    

    The above expands as:

    (DOLIST (#:G1909 (LIST 1 2 3 4)) 
      (MACRO1 (SOME-TEST-P) #:G1909))
    

    The resulting expression contains MACRO1, which will also be expanded. But it is now wrapped in an expression that iterates over some list computed at runtime. Here, it is a constant but you could replace it with any other expression.

    In conclusion, it is often best to combine macros at the macro level, by expanding your own macros into other ones.