Search code examples
lispcommon-lispsbcldefmacro

Correct way to incorporate a docstring in a def* macro?


I am working my way through Practical Common Lisp. I got to the example where you define a deftest macro that works like defun with some added functionality. This got me thinking that it would be nice to be able to add a docstring. I have found that both of the following work, but is one of them more correct? Is there a "right" way to achieve the optional-docstring-like behaviour of defun?

(defmacro deftest (name parameters &body body)
  (let ((docstring ""))
    (when (stringp (car body)) (setf docstring (car body) body (cdr body)))
    `(defun ,name ,parameters
       ,docstring
       (let ((*test-name* (append *test-name* (list ',name))))
         ,@body))))

(defmacro deftest (name parameters &optional docstring &body body)
  (when (not (stringp docstring)) (setf docstring ""))
  `(defun ,name ,parameters
     ,docstring
     (let ((*test-name* (append *test-name* (list ',name))))
       ,@body)))

Solution

  • In general you probably want to parse out both a possible docstring and any declarations from the body of the function, so you can put the declarations where they belong. I use something like this:

    (defun parse-body (body)
        (multiple-value-bind (docstring decls/forms)
            (if (stringp (first body))
                (values (first body) (rest body))
              (values nil body))
          (loop for remainder on decls/forms
                while (and (not (null remainder))
                           (consp (first remainder))
                           (eql (car (first remainder)) 'declare))
                collect (first remainder) into decls
                finally (return (values docstring decls remainder)))))
    

    And then your deftest would be

    (defmacro deftest (name parameters &body body)
      (multiple-value-bind (docstring decls forms) (parse-body body)
        `(defun ,name ,parameters
           ,@(if docstring (list docstring) '())
           ,@decls
           (let ((*test-name* (append *test-name* (list ',name))))
             ,@forms))))
    

    I wish I could say that I have a standard parse-body function, but I don't: I just write a new one each time.