Lisp 宏找不到适用的函数

Lisp Macro cannot find applicable function

给定宏:


(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吗?为什么 reader 找不到通用函数 slot-1

要了解宏的作用,让我们使用 macroexpand

(macroexpand-1 '(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*).
   [Condition of type SB-PCL::NO-APPLICABLE-METHOD-ERROR]

糟糕,同样的错误信息。我将简化宏并删除围绕 slot-1.

的评估
(defmacro sample-macro (p)
  `(if (typep ,p 'sample-class)
      (progn
         (print "evaluated")
         (print (slot-1 p)))))

(macroexpand-1 '(sample-macro *sample-instance*))
=>
(IF (TYPEP *SAMPLE-INSTANCE* 'SAMPLE-CLASS)
    (PROGN (PRINT "evaluated") (PRINT (SLOT-1 P))))

在变量 P 之前,代码看起来不错。那么它会简单地与 ,p 一起使用吗?不需要写 ,(slot-1 p) 因为 slot-1 在这里是正确的。

(defmacro sample-macro (p)
  `(if (typep ,p 'sample-class)
      (progn
         (print "evaluated")
         (print (slot-1 ,p)))))

(macroexpand-1 '(sample-macro *sample-instance*))
=>
(IF (TYPEP *SAMPLE-INSTANCE* 'SAMPLE-CLASS)
    (PROGN (PRINT "evaluated") (PRINT (SLOT-1 *SAMPLE-INSTANCE*))))

代码看起来正确。

(sample-macro *sample-instance*)

"evaluated" 
"sample slot" 

而且有效。

我认为您对宏的作用感到困惑。宏是源代码的运行形式。因此,请考虑当系统尝试扩展宏形式 (sample-macro *sample-instance*) 时会发生什么。在宏展开时,p符号 *sample-instance*:表示一段源代码。

所以现在,看看宏正文中反引号形式:其中有 ,(slot-1 p):这将尝试调用 slot-1 任何 p 绑定到,这是一个符号。然后这失败了,结果宏扩展失败了。

好吧,你可以 'fix' 以一种看起来很明显的方式:

(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*))))

如果您使用宏,它将 'work'。除了它根本不起作用:考虑这种形式:(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)))))

哪个更好。在这种情况下,它甚至是安全的,但在绝大多数情况下,我们需要为我称之为 it:

的东西使用 gensym
(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)))

所以这里的总结是:

根据宏的用途使用宏,即t运行形成源代码。如果您不想这样做,请使用函数。如果您确定 调用 函数的行为占用了大量时间,那么请考虑将它们声明为内联以避免这种情况。

其他答案解释说,宏执行是关于转换源代码和宏扩展时可用的值。

我们也试着理解错误信息。我们需要按字面意思理解:

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]