Lisp宏找不到适用的函数
鉴于宏:
(defclass sample-class ()
((slot-1 :accessor slot-1
:initform "sample slot")))
(defvar *sample-instance*(make-instance 'sample-class))
(defmacro sample-macro (p)
`(if (typep ,p 'sample-class)
(progn
(print "evaluated")
(print ,(slot-1 p)))))
(sample-macro *sample-instance*)
我很困惑为什么这是错误输出
Execution of a form compiled with errors.
Form:
(SAMPLE-MACRO *SAMPLE-INSTANCE*)
Compile-time error:
(during macroexpansion of (SAMPLE-MACRO *SAMPLE-INSTANCE*))
There is no applicable method for the generic function
#<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::SLOT-1 (1)>
when called with arguments
(*SAMPLE-INSTANCE*).
See also:
The ANSI Standard, Section 7.6.6
[Condition of type SB-INT:COMPILED-PROGRAM-ERROR]
宏不应该在过程中扩展和评估s-form吗?为什么读者找不到通用函数slot-1?
回答
我认为您对宏的作用感到困惑。宏是源代码的转换。所以考虑一下当系统尝试扩展宏表单时会发生什么(sample-macro *sample-instance*)。在宏扩展时,p是一个符号 *sample-instance*:表示源代码的位。
所以现在,看看宏主体中的反引号形式:其中有,(slot-1 p): this 将尝试调用绑定到的slot-1任何东西p,它是一个符号。这随后失败,结果宏展开失败。
好吧,你可以用一种看起来很明显的方式“修复”这个问题:
(defmacro sample-macro (p)
`(if (typep ,p 'sample-class)
(progn
(print "evaluated")
(print (slot-1 ,p)))))
这似乎有效。使用宏扩展跟踪器:
(sample-macro *sample-instance*)
-> (if (typep *sample-instance* 'sample-class)
(progn (print "evaluated") (print (slot-1 *sample-instance*))))
如果您使用宏,它将“起作用”。除了它根本不起作用:考虑这种形式(sample-macro (make-instance 'sample-class))::好吧,让我们使用宏跟踪器来看看它:
(sample-macro (make-instance 'sample-class))
-> (if (typep (make-instance 'sample-class) 'sample-class)
(progn
(print "evaluated")
(print (slot-1 (make-instance 'sample-class)))))
哦亲爱的。
所以我们可以通过像这样重写宏来解决这个问题:
(defmacro sample-macro (p)
`(let ((it ,p))
(if (typep it 'sample-class)
(progn
(print "evaluated")
(print (slot-1 it)))
现在
(sample-macro (make-instance 'sample-class))
-> (let ((it (make-instance 'sample-class)))
(if (typep it 'sample-class)
(progn (print "evaluated") (print (slot-1 it)))))
哪个更好。在这种情况下,它甚至是安全的,但在绝大多数情况下,我们需要对我调用的东西使用 gensym it:
(defmacro sample-macro (p)
(let ((itn (make-symbol "IT"))) ;not needed for this macro
`(let ((,itn ,p))
(if (typep ,itn 'sample-class)
(progn
(print "evaluated")
(print (slot-1 ,itn)))))))
现在:
(sample-macro (make-instance 'sample-class))
-> (let ((#:it (make-instance 'sample-class)))
(if (typep #:it 'sample-class)
(progn (print "evaluated") (print (slot-1 #:it)))))
所以这个(实际上也是它的前一个版本)终于可以工作了。
但是等等,但是等等。我们所做的就是把这件事变成这样:
- 将其参数的值绑定到一个变量;
- 并使用该绑定评估一些代码。
有一个名字可以用来做这件事,这个名字是function。
(defun not-sample-macro-any-more (it)
(if (typep it 'sample-class)
(progn
(print "evaluated")
(print (slot-1 it)))))
这完成了工作版本所做的一切,sample-macro但没有所有不必要的复杂性。
嗯,它不做一件事:它不会内联扩展,也许这意味着它可能会慢一点。
好吧,回到燃煤 Lisp 的时代,这是一个真正的问题。燃煤 Lisp 系统具有由木屑和锯末制成的原始编译器,并且在确实非常慢的计算机上运行。所以人们会写一些语义上应该是宏的东西,这样木头编译器就会内联代码。有时这甚至是值得的。
但是现在我们有了先进的编译器(可能仍然主要由木屑和锯末制成),我们可以说出我们的实际意思:
(declaim (inline not-sample-macro-any-more))
(defun not-sample-macro-any-more (it)
(if (typep it 'sample-class)
(progn
(print "evaluated")
(print (slot-1 it)))))
现在您可以合理地保证not-sample-macro-any-more将内联编译。
在这种情况下甚至更好(但代价是几乎可以肯定没有内联内容):
(defgeneric not-even-slightly-sample-macro (it)
(:method (it)
(declare (ignore it))
nil))
(defmethod not-even-slightly-sample-macro ((it sample-class))
(print "evaluated")
(print (slot-1 it)))
所以这里的总结是:
将宏用于它们的用途,即转换源代码。如果您不想这样做,请使用函数。如果您确定调用函数的行为占用了大量时间,请考虑将它们声明为内联以避免这种情况。
回答
其他答案解释说,宏执行是关于转换宏扩展时可用的源代码和值。
让我们也试着理解错误信息。我们需要从字面上理解:
Execution of a form compiled with errors.
上面说它是关于编译的。
Form:
(SAMPLE-MACRO *SAMPLE-INSTANCE*)
以上是要编译的源表。
Compile-time error:
(during macroexpansion of (SAMPLE-MACRO *SAMPLE-INSTANCE*))
再次:编译,现在特别是在宏扩展期间。
There is no applicable method for the generic function
#<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::SLOT-1 (1)>
when called with arguments
(*SAMPLE-INSTANCE*).
现在上面是有趣的部分:没有适用于泛型函数SLOT-1和参数的方法*SAMPLE-INSTANCE*。
什么是*SAMPLE-INSTANCE*?这是一个符号。在您的代码中有一个方法,但它用于 class 的实例sample-class。但是没有符号的方法。所以这行不通:
(setf p '*sample-instance*)
(slot-1 p)
这基本上就是你的代码所做的。您希望使用运行时值,但您在编译时得到的只是一个源符号......
显示带有源代码元素的编译时错误的编译器错误消息表明存在运行时和宏扩展时间计算的混淆。
See also:
The ANSI Standard, Section 7.6.6
[Condition of type SB-INT:COMPILED-PROGRAM-ERROR]