3

I have a macro that intends to creates a closure:

; -*- lexical-binding: t -*-
(defmacro repro ()
  (let ((kmap-sym (gensym "kmap-")))
    `(let ((,kmap-sym (make-sparse-keymap)))
       (define-key ,kmap-sym "a"
         (lambda () (interactive) (message "kmap is %s" ,kmap-sym)))
       ,kmap-sym)))

;; only works when lexical-binding: t in the current file
(funcall (lookup-key (repro) "a"))

Although lexical-binding is t in the file where the macro is defined, (funcall (lookup-key (repro) "a")) fails with the error (void-variable kmap-30252) whenever it is evaluated in a different file where lexical-binding is nil.

Since I can't control where the macro will be used, I considered going back to using lexical-let for the inner let in the example, which does work regardless of where the macro is invoked.

(defmacro repro ()
  (let ((kmap-sym (gensym "kmap-")))
    `(lexical-let ((,kmap-sym (make-sparse-keymap)))
       (define-key ,kmap-sym "a"
         (lambda () (interactive) (message "kmap is %s" ,kmap-sym)))
       ,kmap-sym)))

;; this works in any file
(funcall (lookup-key (repro) "a"))

But lexical-let requires cl, which I would like to avoid, and unfortunately lexical-let is not defined in cl-lib.

What options do I have for creating a closure?

Should the macroexpansion of repro always expand into lexical bindings given that lexical-binding is t where the macro is defined?

erjoalgo
  • 853
  • 1
  • 5
  • 18

3 Answers3

3

Your example is likely not representative of the actual code you're using, but a good solution might be to create the closure in the macro and return the closure, instead of returning code which may or may not turn into a closure depending on lexical-binding:

(defmacro repro ()
  (let* ((kmap-sym (gensym "kmap-")))
    `(let ((,kmap-sym (make-sparse-keymap)))
       (define-key ,kmap-sym "a"
          ',(lambda () (interactive) (message "kmap is %S" (symbol-value kmap-sym))))
       ,kmap-sym)))

The other option I use nowadays is to do something like

(defmacro ...
  (if (not lexical-binding)
      (error "Macro `foo` can't be used with dynamic binding"))
    ...))

the variable lexical-binding can be relied upon (while expanding the macro) to indicate whether the returned code will be run with lexical or dynamic binding.

Stefan
  • 26,154
  • 3
  • 46
  • 84
2

There is an ugly workaround using eval with the optional argument LEXICAL set to t.

(defmacro repro ()
  (let ((kmap-sym (gensym "kmap-")))
    `(eval
      '(let ((,kmap-sym (make-sparse-keymap)))
         (define-key ,kmap-sym "a"
           (lambda () (interactive) (message "kmap is %s" ,kmap-sym)))
         ,kmap-sym)
      t)))

It is ugly since the associated quoting prevents byte-compilation.

Tobias
  • 32,569
  • 1
  • 34
  • 75
  • it's nice to know this exists, although I'd prefer `(eval-when-compile (require 'cl-lib))` over this. – erjoalgo Nov 12 '18 at 21:32
1

Not very sure if the result meets your expectation.

(defun foo (keymap)
  `(lambda () (interactive) (message "kmap is %s" ',keymap)))

(defmacro repro ()
  (let ((kmap-sym (gensym "kmap-")))
    `(let ((,kmap-sym (make-sparse-keymap)))
       (define-key ,kmap-sym "a"
         (foo ,kmap-sym))
       ,kmap-sym)))

(setq lexical-binding nil)
;; => nil

(funcall (lookup-key (repro) "a"))
;; => "kmap is (keymap (97 lambda nil (interactive) (message kmap is %s (quote #0))))"

(setq lexical-binding t)
;; => t

(funcall (lookup-key (repro) "a"))
;; => "kmap is (keymap (97 lambda nil (interactive) (message kmap is %s (quote #0))))"

The following does the same without using a top-level defun.

(defmacro repro ()
  (let ((kmap-sym (gensym "kmap-"))
        (fun (lambda (keymap)
               `(lambda () (interactive) (message "kmap is %s" ',keymap)))))
    `(let ((,kmap-sym (make-sparse-keymap)))
       (define-key ,kmap-sym "a"
         (,fun ,kmap-sym))
       ,kmap-sym)))
xuchunyang
  • 14,302
  • 1
  • 18
  • 39
  • I was considering something like this based on the way one can create a javascript closure. But I wasn't sure if there was another alternative. I was also hoping to provide this closure anonymously instead of relying on a top-level function. – erjoalgo Nov 12 '18 at 22:35
  • @erjoalgo You can put the top-level function into the macro, (let ((f (lambda (keymap) `(lambda () ...))))), it does the same. – xuchunyang Nov 12 '18 at 22:51