Search code examples
common-lispsymbolskeyword-argument

Lambda List Error with &rest and &key arguments in Common Lisp


The following function aims to make a symbol out of several arguments. However, calling it generates a keyword error.

(defun create-symbol (&rest objects &key intern (package *package*))
  "Creates a symbol from the objects."
  (let ((arg-string (format nil "~{~A~^~}" (first objects))))
    (if intern
        (values (intern arg-string package))
      (make-symbol arg-string))))

For example, (create-symbol "A" 1) produces Unknown &KEY argument: "A" instead of #:A1.

Also not sure if (first objects) is the correct way to access the &rest arguments, if no keywords are submitted.

Thanks for any help verifying the intended operation of this function.

Edit: Given the comments below, it looks like manual parsing of the arguments may be one way to go when they are combinations of the lambda list keywords &optional, &rest, and &key. The following function seems to do what I was originally intending:

(defun create-symbol (&rest objects&keys)
  "Creates a symbol from the objects,
   with optional keywords :intern and :package."
  (let* ((keys (member-if #'keywordp objects&keys))
         (objects (ldiff objects&keys keys))
         (arg-string (format nil "~{~A~^~}" objects)))
    (if (getf keys :intern)
      (intern arg-string (or (getf keys :package) *package*))
      (make-symbol arg-string))))

Solution

  • A very small example showing what's going wrong is:

    (defun test-key-rest (&rest args &key a (b t))
        (list 'args args 'a a 'b b))
    (test-key-rest :a 1 :b 2); => (ARGS (:A 1 :B 2) A 1 B 2)
    (test-key-rest :a 1 :b 2 "rest now?");;; Error: The passed key "rest now?" is not defined for this function
    (test-key-rest :a 1 :b 2 :c 3);;; Error: The passed key :C is not defined for this function
    

    One could perhaps use &allow-other-keys, but I think it would be messy. I remembered reading about this kind of situation in Practical Common Lisp, where Peter Seibel writes (emphasis mine):

    The other two combinations, either &optional or &rest parameters combined with &key parameters, can lead to somewhat surprising behavior.

    I suggest separating the two argument lists. destructuring-bind makes it easy:

    (defun test-two-arg-lists (keys &rest args)
        (destructuring-bind (&key (a nil) (b t)) keys
            (list 'a a 'b b 'args args)))
    (test-two-arg-lists (list :a 1) "more" "please"); => (A 1 B T ARGS ("more" "please"))
    

    But I (and I assume others) don't want to have to construct that first keyword argument list, so let's make it evaluate its arguments as we'd expect with a macro:

    (defmacro test-two-nice (keys &rest args)
        `(test-two-arg-lists (list ,@keys) ,@args))
    (test-two-nice (:a 1) "more" "please"); => (A 1 B T ARGS ("more" "please"))
    

    So to pull it all together:

    (defun create-symbol-fn (keys &rest objects)
      "Creates a symbol from the objects."
      (destructuring-bind (&key intern (package *package*)) keys
        (let ((arg-string (format nil "~{~A~}" objects)))
          (if intern
              (values (intern arg-string package))
            (make-symbol arg-string)))))
    (defmacro create-symbol (keys &rest objects)
      `(create-symbol-fn (list ,@keys) ,@objects))
    (create-symbol (:intern nil) 'a 'b 'c 'd); => #:ABCD