Elisp:加载时使用未定义变量的 eval 进行宏扩展

Elisp: Macro expansion at load time with eval of undefined variable

这里是 Lisp 菜鸟。

我的目标是定义一个宏,使点分列表的键可以作为变量访问相应的值,因此得名 »let-dotted-alist«。所以我想要的是:

(setq foo '((a . "aa") (b . "bb")))

(let-dotted-alist foo
(message (concat a b)))
                           ==> "aabb"

这是迄今为止我能想到的最好的:

(defmacro let-dotted-alist (alist &rest body)
"binds the car of each element of dotted ALIST to the corresponding cdr and makes them available  as variables in BODY."
  `(let ,(nreverse 
      (mapcar    
       (lambda (p) (list (car p) (cdr p))) 
       (eval alist)))
     ,@body))

这里的问题是eval。我需要它以便能够将 alist 作为变量 (foo) 而不是文字传递,这是定义函数时的主要用例。为了让宏计算出代码,这个变量需要已经绑定。仍然到处都是我读到的,使用 eval 往往表明代码中存在缺陷?有解决办法吗?

如果 Emacs 24 没有引入想要在加载时扩展宏的急切宏扩展,那么这将是一个有点学术性的问题,当变量(dotlist 在以下示例中)应该提供alist 仍然无效:

(defun concat-my-cdrs (dotlist)
  (let-dotted-alist dotlist
            (print (concat a b))))

根据我对此的评估方式,我要么得到 »mapcar:Symbol's value as variable is void: dotlist« 要么 »Eager macro-expansion failure: (void-variable dotlist)«。这当然是有道理的,因为变量 dotlist 在加载时确实是空的。

现在,在我尝试按照(本地)禁用急切宏扩展的思路找到解决方法之前,是否有任何方法可以改进宏定义以完全避免 eval

我不相信你可以避免 eval,问题的陈述方式:macroexpand let-bindings depending on a variable。不使用 eval 的唯一方法是将变量评估推迟到 macroexpand 之后,但这与扩展 let 绑定的要求相矛盾。我想我在说一些对你来说显而易见的事情。

所以问题显然是您的要求是否可以更改以消除 eval 的需要。这意味着要么避免 let-绑定,要么在宏参数中明确绑定。是否可以接受由您决定,但我个人的看法是 - eval 并没有坏到扼杀您的用例。我会把 eval 放在那里。 (我最近在 Clojure 中做了基本相同的事情,我需要将相同的局部符号绑定到不同的值,模拟 OCaml 仿函数。这是一个题外话来解释为什么我可能偏向于保持你这样做的方式。)

我可能不知道一些特定于 elisp 的技巧 - 虽然即使那样我可能更喜欢 eval 而不是一些我迄今为止从未遇到过的技巧。

首先,我会提到 eager-macroexpansion 不会引入新问题:如果您尝试对文件进行字节编译,早期的 Emacsen 中也会出现同样的问题。

至于避免 eval,你可以通过使用其他使用 eval 的东西,例如cl-progv:

(defmacro let-dotted-alist (alist &rest body)
  (macroexp-let2 nil alist alist
    `(cl-progv (mapcar #'car ,alist)
               (mapcar #'cdr ,alist)
       ,@body)))

但请注意语义有点不同:变量只能是动态作用域而不是词法作用域,因为变量列表仅在 运行 时间才知道。