首页 > 解决方案 > `apply` 或 `funcall` 用于宏而不是函数

问题描述

在 Lisp 中,函数的参数在进入函数体之前首先被求值。宏参数保持不被评估。

但有时,人们想将存储在变量中的代码片段注入到宏中。这意味着首先评估宏的参数,然后将选择的宏应用于此评估结果。

不得不求助于

(eval `(macro ,arg))

实现这一点 - 但eval在不同的环境中行为不正确。

最好的事情是,如果可以做到:

(apply macro (list arg))

或者

(funcall macro arg)

但是由于宏不是一个函数,所以这是行不通的。

有可能实现这样的目标吗?- 为了规避这个问题,让宏在函数命名空间中可用?

还是我错过了解决此类问题的其他方法?

我在尝试回答如何从列表中生成 HTML 时遇到了这个问题。但也在通用 lisp 中使用宏生成 TYPECASE 中评估传递给在 lisp 中生成函数的宏的参数,以及如何在方案中将列表转换为代码/lambda?. 但我一直认为,在回答他们时,最好有一个可以接受宏的apply或类似的函数。funcall

标签: macroscommon-lispapplyevalfuncall

解决方案


目前尚不清楚您要做什么,尽管几乎可以肯定您对某些事情感到困惑。特别是如果您eval在宏扩展内部调用,那么在几乎所有情况下,您都在做一些严重错误和严重危险的事情。我从来没有想过我想要宏扩展到包括在内的东西的情况,eval并且我已经编写了 Lisp 很长时间了。


话虽如此,这就是您如何调用与宏相关联的函数,以及为什么它很少是您想要做的。

宏只是其域和范围是源代码的函数:它们是从一种语言到另一种语言的编译器。完全可以调用与宏关联的函数,但是该函数将返回源代码,然后您需要对该源代码进行评估。如果您想要一个处理不是源代码的运行时数据的函数,那么您需要该函数,并且您不能通过某种魔术将宏转换为该函数,这似乎是您想要做的:魔术不存在,也不可能存在

例如,如果我有一个宏

(defmacro with-x (&body forms)
  `(let ((x 1))
     ,@forms))

然后我可以在一点源代码上调用它的宏函数:

> (funcall (macro-function 'with-x)
                     '(with-x (print "foo")) nil)
(let ((x 1)) (print "foo"))

但是这样做的结果是另外一点源代码:我需要编译或评估它,而我无能为力。

确实在(几乎?)所有情况下,这与macroexpand-1)相同:

> (macroexpand-1 '(with-x (print "foo")))
(let ((x 1)) (print "foo"))
t

你可能可以这样写macroexpand-1macro-function

(defun macroexpand-1/equivalent (form &optional (env nil))
  (if (and (consp form)
           (symbolp (first form))
           (macro-function (first form)))
      (values (funcall (macro-function (first form)) form env)
              t)
    (values form nil)))

那么,如果调用宏的结果是源代码,那么你如何处理该源代码以获得不是源代码的结果?好吧,你必须评估它。然后,好吧,既然评估器无论如何都会为你扩展宏,你不妨写一些类似的东西

(defun evaluate-with-x (code)
  (funcall (compile nil `(lambda ()
                           (with-x ,@code)))))

所以你在任何情况下都不需要调用宏的函数。这并不是将宏转换为处理不是源代码的数据的函数的魔术:这是一个完全由爆炸部分组成的可怕恐怖。

一个具体的例子:CL-WHO

看起来这个问题可能源于这个问题,而潜在的问题是这不是 CL-WHO 所做的。尤其是认为像 CL-WHO 这样的工具是用于获取某种列表并将其转换为 HTML 的工具,这是一种困惑。它不是:它是一种获取基于 CL 的语言的源代码的工具,但包括一种表达与 CL 代码混合的 HTML 输出的方式,并将其编译成 CL 代码,它会做同样的事情。恰好是 CL 源代码表示为列表和符号的情况,但 CL-WHO 并不是真的:它是一个从“CL-WHO 语言”到 CL 的编译器。

所以,让我们试试我们上面尝试的技巧,看看为什么它是一场灾难:

(defun form->html/insane (form)
  (funcall 
   (compile nil `(lambda () 
                   (with-html-output-to-string (,(make-symbol "O"))
                     ,@form)))))

如果你没有仔细观察这个,你可能会认为这个函数实际上确实起到了神奇的作用:

> (form->html/insane '(:p ((:a :href "foo") "the foo")))
"<p></p><a href='foo'>the foo</a>"

但事实并非如此。如果我们调用form->html/insane这个完全无害的列表会发生什么:

(:p (uiop/run-program:run-program "rm -rf $HOME" :output t))

提示:如果您没有很好的备份,请不要访问form->html/insane此列表。

CL-WHO 是一种编程语言的实现,它是 CL 的严格超集:如果您尝试将其转换为将列表转换为 HTML 的函数,那么您最终会得到涉及每次调用时都在修补的同一个核武器eval,除了核武器藏在一个锁着的柜子里,你看不到它。但它并不关心这一点:如果你把它点燃,它仍然会将几英里内的所有东西都变成放射性灰烬和瓦砾。

因此,如果您想要一个将列表(不是源代码的列表)转换为 HTML 的工具,请编写该工具。CL-WHO 在其实施中可能有这样一个工具的胆量,但你不能按原样使用它。


每当您试图以这种方式滥用宏时,您都会遇到同样的问题:调用宏函数的结果是Lisp 源代码,并评估您需要的源代码evaleval. 而且eval不仅不是解决几乎所有问题的糟糕解决方案:它还是核武器。对于某些问题,核武器也许是很好的解决方案,但它们很少而且相差甚远。


推荐阅读