你能在Emacs Lisp宏中创建交互式函数吗?

10
我正在尝试编写一个emacs lisp宏来创建一些“辅助函数”。最终,我的辅助函数将比我现在拥有的更有用。我意识到可能有更好/更直观的方法来实现同样的事情(请发表),但我的基本问题是:为什么这样不起作用/我做错了什么:
(defmacro deftext (functionname texttoinsert)
  `(defun ,(make-symbol (concatenate 'string "text-" functionname)) ()
     (interactive)
     (insert-string ,texttoinsert)))

(deftext "swallow" "What is the flight speed velocity of a laden swallow?")
(deftext "ni" "What is the flight speed velocity of a laden swallow?")

如果我使用宏展开的输出并对其进行评估,我将获得与宏意图相同的交互函数,但即使宏运行并似乎已经评估,我也无法调用M-x text-nitext-swallow

4个回答

13

这样做可以达到你想要的效果:

(defmacro deftext (functionname texttoinsert)
  (let ((funsymbol (intern (concat "text-" functionname))))
`(defun ,funsymbol () (interactive) (insert-string ,texttoinsert))))

intern 做到了这一点。我甚至不需要 'let'。(defmacro deftext (functionname texttoinsert) `(defun ,(intern (concatenate 'string "text-" functionname)) () (interactive) (insert-string ,texttoinsert)))之前我已经尝试使用 intern,但我认为我调用方法错误了。 谢谢! - Kelly McDonald

4
正如所指出的那样,解决方案是使用 `intern` 而不是 `make-symbol`。
可以创建多个具有相同名称的独立符号,但其中只有一个可以成为给定名称的规范符号——即在其他地方引用时将获得的符号。
`intern` 返回给定名称的规范符号。仅当不存在该名称的已内部化符号时,它才会创建新符号。这意味着它只会为任何给定名称创建一个符号。
另一方面,`make-symbol` 每次调用时都会创建一个新符号。这些是未内部化的符号——每个符号都是完全有效和可用的符号,但不是通过其名称引用时将看到的符号。
请参见:`C-h i g (elisp) Creating Symbols`
因为 `defun` 返回它设置的符号,所以您可以通过捕获返回值并将其用作函数来观察正在发生的情况:
(defalias 'foo (deftext "ni" "What is the flight speed velocity of a laden swallow?"))
M-x text-ni ;; doesn't work
M-x foo ;; works

或类似:
(call-interactively (deftext "shrubbery" "It is a good shrubbery. I like the laurels particularly."))

在所有这些中最棘手的部分——也是评估扩展形式做你想要的事情,但宏没有做到的原因——与Lisp阅读器如何以及何时(或者是否)将函数名称转换为符号有关。

如果我们编写一个名为foo1的函数:

(defun foo1 (texttoinsert) (insert-string texttoinsert))

lisp阅读器将其作为文本读取,并将其转换为lisp对象。我们可以使用read-from-string做同样的事情,然后我们可以查看生成的lisp对象的打印表示:

ELISP> (car (read-from-string "(defun foo1 (texttoinsert) (insert-string texttoinsert))"))
(defun foo1
    (texttoinsert)
  (insert-string texttoinsert))

在这个对象中,函数名是具有名称“foo1”的规范符号。请注意,我们从read-from-string看到的返回值仅为该对象的打印表示,规范符号仅由其名称表示。打印表示不能使我们区分已内部化和未内部化的符号,因为所有符号都仅由其名称表示。
(暂时跳过,当评估宏的打印形式时,这是您遇到问题的源头,因为该打印形式通过lisp读取器传递,曾经是未内部化的符号成为内部化的符号。)
如果我们继续讨论宏:
(defmacro deffoo2 ()
  `(defun foo2 (texttoinsert) (insert-string texttoinsert)))

ELISP> (car (read-from-string "(defmacro deffoo2 ()
        `(defun foo2 (texttoinsert) (insert-string texttoinsert)))"))
(defmacro deffoo2 nil
  `(defun foo2
       (texttoinsert)
     (insert-string texttoinsert)))

这次读者已经将定义读成一个lisp对象,在该对象中有一个规范符号foo2。我们可以直接检查这个对象来验证:

ELISP> (eq 'foo2
           (cadr (cadr (nth 3
            (car (read-from-string "(defmacro deffoo2 ()
             `(defun foo2 () (insert-string texttoinsert)))"))))))
t

所以对于这个宏来说,它已经在任何宏调用/扩展发生之前处理了那个规范符号foo2,因为当读取宏自身时,lisp读取器已经建立了这种关系。与我们之前简单的函数定义不同(其中函数符号由Lisp读取器确定并定义),当调用和扩展宏时,不会使用Lisp读取器。宏展开是使用预先存在的Lisp对象执行的 - 无需进行读取操作。
在此示例中,函数符号已经出现在宏中,因为它是规范符号,我们可以在代码的其他位置使用(foo2)来调用该函数。(或者,如果我使定义交互式,可以使用M-x foo2。)
最后回到原始问题中的宏,很明显lisp读取器永远不会遇到将定义函数名称符号:
ELISP> (car (read-from-string "(defmacro deftext (functionname texttoinsert)
        `(defun ,(make-symbol (concatenate 'string \"text-\" functionname)) ()
           (interactive)
           (insert-string ,texttoinsert)))"))
(defmacro deftext
    (functionname texttoinsert)
  `(defun ,(make-symbol
            (concatenate 'string "text-" functionname))
       nil
     (interactive)
     (insert-string ,texttoinsert)))

相反,由lisp阅读器产生的对象包含表达式,(make-symbol (concatenate 'string "text-" functionname));并且该反引号表达式将在扩展时评估,以创建一个新的未注册符号,该符号将成为由该扩展创建的对象的一部分。
在我们之前的示例中,生成的对象具有defun(已注册)的car和foo1foo2(两者都已注册)的cadr。
在这个最后的例子中,对象具有defun(已注册)的car,但是一个未注册符号的cadr(名称来自concatenate表达式的结果)。
最后,如果您打印该对象,则未注册函数符号的打印表示形式将是符号名称,并且通过评估它来读取该打印表示形式会导致定义规范符号的函数单元格。 1实际上,可以使用unintern函数取消注册符号,此后对于相同名称调用intern自然会创建一个新符号; 但是,这对于本讨论并不重要。

3

如果你使用lexical-binding,你就不需要使用宏:

(defun deftext (functionname texttoinsert)
  (defalias (intern (concat "text-" functionname))
    (lambda ()
      (interactive)
      (insert-string texttoinsert))))

1

虽然已经过去了几年,但我认为您可能缺少一个fset来定义该函数;如果您希望在编译时使用它,请参见文档


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接