Rorschach
Rorschach

Reputation: 32466

Expanding macros inside dolist

How can I expand a macro that adds symbols to the obarray (here defun) inside another loop macro like a dolist? For example,

(defmacro make-cmd (cmd &optional search)
  "Some function factory."
  (let ((fn (intern (concat "fn-" cmd))))
    `(defun ,fn (&optional args)
       (interactive)
       (let ((str (symbol-name ',fn))
             ,@(when search
                 '((dir "~"))))
         (message "Called %S from %S" str
                  (or (and (bound-and-true-p dir) dir)
                      default-directory))))))

;; functions `fn-f1' and `fn-f2' aren't added to obarray
(dolist (x '("f1" "f2"))
  `(make-cmd ,x t))

;; works like this
(make-cmd "f1" t)

I would like to be able to just require the macros when compiling and loop over function names. A common-lisp solution would probably be fine to adapt to emacs-lisp.

Upvotes: 2

Views: 411

Answers (3)

Drew
Drew

Reputation: 30718

(Others have answered your direct question. But this might answer the question behind your question and speak to what you are really trying to do. If not, see the other answers.)

You do not need a macro at all, to do what you want. Just use defalias or fset. Each is a function.

(defun foo (cmd &optional search)
  (let ((fn  (intern (concat "fn-" cmd))))
    (defalias fn `(lambda (&optional args)
                    (let ((str  (symbol-name ',fn))
                          ,@(when search
                                  '((dir "~"))))
                      (message "Called %S from %S" str
                               (or (and (bound-and-true-p 'dir) dir)
                                   default-directory)))))))

(dolist (x  '("f1" "f2")) (foo x))

Then (symbol-function 'fn-f1) returns:

(lambda (&optional args)
  (let ((str  (symbol-name 'fn-f1)))
    (message "Called %S from %S" str (or (and (bound-and-true-p dir) dir)
                                         default-directory))))

And if you use lexical binding (i.e., put the local-variable binding -*- lexical-binding: t -*- at the top of the file where this code is defined, then you don't need any of the backquoting. For example:

(defun foo (cmd &optional search)
  (let ((fn  (intern (concat "fn-" cmd))))
    (defalias fn (lambda (&optional args)
                    (let ((str  (symbol-name fn))
                          (dir  (if search "~" default-directory)))
                      (message "Called %S from %S" str dir))))))

(dolist (x  '("f1" "f2")) (foo x))

If you do that then each function fn-f1 and fn-f2 is defined as a closure, which looks like this:

(symbol-function 'fn-f1)

(closure
 ((fn . fn-f1)
  (search)
  (cmd . "f1")
  t)
 (&optional args)
 (let ((str  (symbol-name fn))
       (dir  (if search "~" default-directory)))
   (message "Called %S from %S" str dir)))

And (foo "f3" :SEARCH) defines a function fn-f3 (a closure) that encapsulates a binding of otherwise free variable search to the non-nil value :SEARCH, so that local variable dir becomes bound to "~":

(symbol-function 'fn-f3)

(closure
 ((fn . fn-f3)
  (search . :SEARCH)  ;; <==============
  (cmd . "f3")
  t)
 (&optional args)
 (let ((str  (symbol-name fn))
       (dir  (if search "~" default-directory)))
   (message "Called %S from %S" str dir)))

Upvotes: 3

Rainer Joswig
Rainer Joswig

Reputation: 139411

Another typical solution in Common Lisp is to write a macro defcmds so that you can write:

(defcmds ("f1" 1) ("f2" t))

Which one would make expand into:

(progn
  (defcmd "f1" t)
  (defcmd "f2" t))

Where thedefcmd forms would expand into

(progn
  (defun ...)
  (defun ...))

Upvotes: 2

Kaz
Kaz

Reputation: 58647

You need:

(dolist (x '("f1" "f2"))
  (eval `(make-cmd ,x t)))

The backquote expression `(make-cmd ,x t) only constructs the syntax. That syntax isn't evaluated. It's the same thing as if you had written (list 'make-cmd x t).

If the primary use for this make-cmd macro calls for eval, it might as well be turned into a function:

(defun make-cmd-fun (cmd &optional search)
  "Some function factory."
  (let ((fn (intern (concat "fn-" cmd))))
    `(defun 
       ...)))

Now you have a plain function which returns defun syntax; and that of course has to be eval-ed to put it into effect. But now we don't have to construct any more syntax at the call site where we use the macro:

(dolist (x '("f1" "f2"))
  ;; Note how at least the backquote is gone (but not eval).
  (eval (make-cmd-fun x t))) ;; execute the defun form returned by make-cmd-fun

When we have make-cmd-fun, we can then define the macro version in terms of it:

(defmacro make-cmd (cmd &optional search)
  (make-cmd-fun cmd search))

Basically we have made the raw macro expander function available as make-cmd-fun; the macro make-cmd just calls that expander.

Upvotes: 2

Related Questions