cstml
cstml

Reputation: 390

Lisp Macro cannot find applicable function

Given the macro:


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

I am confused as to why this is the error output

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]

Shouldn't the macro expand and evaluate the s-form in the process? Why is the reader not finding the generic function slot-1?

Upvotes: 2

Views: 122

Answers (3)

Rainer Joswig
Rainer Joswig

Reputation: 139401

The other answers explained that macro execution is about transforming source code and values available at macro expansion time.

Let's also try to understand the error message. We need to take it literally:

Execution of a form compiled with errors.

Above says that it is about compilation.

Form:
  (SAMPLE-MACRO *SAMPLE-INSTANCE*)

Above is the source form which is to be compiled.

Compile-time error:
  (during macroexpansion of (SAMPLE-MACRO *SAMPLE-INSTANCE*))

Again: compilation and now specifically during macro expansion.

There is no applicable method for the generic function
  #<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::SLOT-1 (1)>
when called with arguments
  (*SAMPLE-INSTANCE*).

Now above is the interesting part: there is no applicable method for the generic function SLOT-1 and the argument *SAMPLE-INSTANCE*.

What is *SAMPLE-INSTANCE*? It's a symbol. In your code is a method, but it is for instances of class sample-class. But there is no method for symbols. So this will not work:

(setf p '*sample-instance*)
(slot-1 p)

That's basically what your code did. You expected to work with runtime values, but all you got at compile time was a source symbol...

The compiler error message showing a compile time error with a source-code element is an indication that there is mixup of runtime and macro-expansion time computation.

See also:
  The ANSI Standard, Section 7.6.6
   [Condition of type SB-INT:COMPILED-PROGRAM-ERROR]

Upvotes: 5

user5920214
user5920214

Reputation:

I think you are confused about what macros do. Macros are transformations of source code. So consider what happens when the system tries to expand the macro form (sample-macro *sample-instance*). At macroexpansion time, p is the symbol *sample-instance*: a representation of a bit of source code.

So now, look at the backquoted form in the body of the macro: in it there is ,(slot-1 p): this will try and call slot-1 on whatever p is bound to, which is a symbol. This then fails and the macroexpansion fails as a result.

Well, you could 'fix' this in a way which seems obvious:

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

And this seems to work. Using a macroexpansion tracer:

(sample-macro *sample-instance*)
 -> (if (typep *sample-instance* 'sample-class)
        (progn (print "evaluated") (print (slot-1 *sample-instance*))))

And if you use the macro it will 'work'. Except it won't work, at all: Consider this form: (sample-macro (make-instance 'sample-class)): well, let's look at that using the macro tracer:

(sample-macro (make-instance 'sample-class))
 -> (if (typep (make-instance 'sample-class) 'sample-class)
        (progn
          (print "evaluated")
          (print (slot-1 (make-instance 'sample-class)))))

Oh dear.

So we could work around this problem by rewriting the macro like this:

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

And now

(sample-macro (make-instance 'sample-class))
 -> (let ((it (make-instance 'sample-class)))
      (if (typep it 'sample-class)
          (progn (print "evaluated") (print (slot-1 it)))))

Which is better. And in this case it's even safe, but in the great majority of cases we'd need to use a gensym for the thing I've called 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)))))))

And now:

(sample-macro (make-instance 'sample-class))
 -> (let ((#:it (make-instance 'sample-class)))
      (if (typep #:it 'sample-class)
          (progn (print "evaluated") (print (slot-1 #:it)))))

So this (and in fact the previous version of it as well) is finally working.

But wait, but wait. What we've done is to turn this thing into something which:

  • binds the value of its argument to a variable;
  • and evaluates some code with that binding.

There's a name for something which does that, and that name is function.

(defun not-sample-macro-any-more (it)
  (if (typep it 'sample-class)
      (progn
        (print "evaluated")
        (print (slot-1 it)))))

This does everything that the working versions of sample-macro did but without all the needless complexity.

Well, it doesn't do one thing: it doesn't get expanded inline, and perhaps that means it might be a little slower.

Well, back in the days of coal-fired Lisp this was a real problem. Coal-fired Lisp systems had primitive compilers made of wood shavings and sawdust and ran on computers which were very slow indeed. So people would write things which should semantically be functions as macros so the wood-shaving compiler would inline the code. And sometimes this was even worth it.

But now we have advanced compilers (probably still mostly made of wood shavings and sawdust though) and we can say what we actually mean:

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

And now you can be reasonably assured that not-sample-macro-any-more will be compiled inline.

Even better in this case (but at the cost of almost certainly not having the inlining stuff):

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

So the summary here is:

Use macros for what they are for, which is transforming source code. If you don't want to do that, use functions. If you are sure the that the act of calling the functions is taking up lots of time, then consider declaring them inline to avoid that.

Upvotes: 6

Ehvince
Ehvince

Reputation: 18415

To understand what the macro is doing, let's use 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]

Oops, same error message. I will simplify the macro and remove the evaluation around 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))))

The code looks good until the variable P. So will it work with, simply, ,p? No need to write ,(slot-1 p) since slot-1 is here correctly.

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

The code looks correct.

(sample-macro *sample-instance*)

"evaluated" 
"sample slot" 

and it works.

Upvotes: 2

Related Questions