macroslispcommon-lispsbclpractical-common-lisp

Common Lisp Macro Argument Mismatch Despite &rest / &body


I have been reading Peter Seibel's book, Practical Common Lisp, piecing together the project from the book code available online in the order it appears in the book, and so far, I have a file that compiles and loads each chapter's code in turn, and this is where I have run into problems: when I load the FASL for the project so far, I get warnings in the ID3v2 section like this the one below.

I don't understand where the argument number conflict emerges. UNSIGNED-INTEGER seems to be getting its two keyword arguments. Also, it seems to me that the DEFINE-BINARY-TYPE macro will accept any number of arguments with the use of &rest/&body. I was wondering if you had any hints or advice. Some relevant output and code is below. Any and all help is appreciated.

Thanks in Advance,

; file: .../cl-playlist/id3v2.lisp
; in: DEFINE-BINARY-TYPE U1
;     (BINARY-DATA:DEFINE-BINARY-TYPE ID3V2::U1
;         NIL
;       (ID3V2::UNSIGNED-INTEGER :BYTES 1 :BITS-PER-BYTE 8))
; ...
; ==>
;   (BINARY-DATA:READ-VALUE 'ID3V2::UNSIGNED-INTEGER #:STREAM :BYTES 1
;                           :BITS-PER-BYTE 8)
; 
; caught STYLE-WARNING:
;   The function was called with six arguments, but wants exactly two.

The offending function from "id3v2.lisp" looks like this,

(define-binary-type u1 () (unsigned-integer :bytes 1 :bits-per-byte 8))

using

(define-binary-type unsigned-integer (bytes bits-per-byte)
  (:reader (in)
       (loop with value = 0
          for low-bit 
          downfrom (* bits-per-byte (1- bytes)) to 0 by bits-per-byte do
        (setf (ldb (byte bits-per-byte low-bit) value) (read-byte in))
          finally (return value)))
  (:writer (out value)
       (loop for low-bit 
          downfrom (* bits-per-byte (1- bytes)) to 0 by bits-per-byte
          do (write-byte (ldb (byte bits-per-byte low-bit) value) out))))

from the following in "binary-data.lisp"

(defmacro define-binary-type (name (&rest args) &body spec)
; (defmacro define-binary-type (name &rest args &body spec)
  (with-gensyms (type stream value)
  `(progn
    (defmethod read-value ((,type (eql ',name)) ,stream &key ,@args)
      (declare (ignorable ,@args))
      ,(type-reader-body spec stream))
    (defmethod write-value ((,type (eql ',name)) ,stream ,value &key ,@args)
      (declare (ignorable ,@args))
      ,(type-writer-body spec stream value)))))

Solution

  • The problem your code has is that you call a function with a wrong number of arguments. The function has been created with an argument list of fewer elements.

    See this:

    CL-USER> (defmethod foo ((a string) (b string) &key) (list a b))
    STYLE-WARNING:
       Implicitly creating new generic function COMMON-LISP-USER::FOO.
    #<STANDARD-METHOD FOO (STRING STRING) {1005603C53}>
    

    Above says that DEFMETHOD also creates the corresponding generic function, because there was none. That's fine. We could also use DEFGENERIC to declare the generic function and its arguments. Here SBCL infers it from the method it sees.

    The method has exactly two arguments. There are no keyword arguments. Let's call it from another function with a few, supposedly, keyword arguments.

    CL-USER> (defun bar (baz) (foo baz baz :k1 10))
    ; in: DEFUN BAR
    ;     (FOO BAZ BAZ :K1 10)
    ;
    ; caught STYLE-WARNING:
    ;   The function was called with four arguments, but wants exactly two.
    ;
    ; compilation unit finished
    ;   caught 1 STYLE-WARNING condition
    BAR
    CL-USER> 
    

    Now SBCL tells us that we call the generic function with four arguments, even though the generic function has only two parameters.

    In your case the declaration of U1 describes a function with two arguments. There are no keyword arguments for READ-DATA-VALUE.

    There are now several possible ways to deal with this:

    1. use a DEFGENERIC to define the generic function READ-DATA-VALUE with the argument list you really want to use and make sure all your methods follow it.

    2. Put all the arguments in all methods. In methods where they are not used, declare them to be ignorable.

    3. allow other keyword arguments, &allow-other-keys, to allow different methods to have different sets of keyword arguments. Best do it also in the DEFGENERIC form.

    Then:

    CL-USER> (defmethod foo1 ((a string) (b string) &key &allow-other-keys)
               (list a b))
    STYLE-WARNING:                                                                                                                                                                                   
       Implicitly creating new generic function COMMON-LISP-USER::FOO1.                                                                                                                              
    #<STANDARD-METHOD FOO1 (STRING STRING) {10058AC193}>                                                                                                                                             
    CL-USER> (defun bar1 (baz) (foo1 baz baz :k1 10))
    BAR1                                                                                                                                                                                             
    

    You can see that SBCL no longer complains and assumes that the arguments are keyword arguments.

    The drawback is that the Lisp compiler now assumes that you can use arbitrary keyword arguments for this generic function and can't tell you (also not at compile time) if you pass a wrong one.