Search code examples
macroslispcommon-lisp

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?


Solution

  • 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.