davypough
davypough

Reputation: 1941

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

Upvotes: 3

Views: 966

Answers (2)

Jonathan Johansen
Jonathan Johansen

Reputation: 363

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

Upvotes: 2

Dan Robertson
Dan Robertson

Reputation: 4360

Basically you can’t do it this way. Either make intern not a keyword argument or do your own keyword parsing. Here are the rules for argument parsing for ordinary functions:

  1. A function has three kinds of arguments for parsing purposes: required, optional, and rest
  2. Any arguments which appear before a lambda list keyword (e.g. &optional) are required arguments. They must be passed. For further steps, only the arguments passed after the required arguments are counted.
  3. After the required arguments in the lambda list may come &optional and then optional arguments. If there are any passed arguments yet to be parsed, these are taken as optional arguments until no optional arguments are left to parse. If there are no arguments left to parse then we are done.
  4. After the optional arguments (if any) there can be a rest argument (&rest followed by a symbol to bind), keyword arguments (prefixed by &key with the &allow-other-keys keyword modifying the parsing. Any passed arguments not yet parsed at this stage are called rest arguments. This is how they are parsed:
    1. If there was a &rest argument in the lambda list, bind it to whatever arguments haven’t been parsed.
    2. If there are any keyword arguments then require the number of rest arguments to be even and for each argument left, read first a key and match it to the symbol of an unbound keyword argument. Bind that argument to the next rest argument. If a key is repeated it is an error. If a key is unknown it is an error unless the &allow-other-keys keyword was specified.

One can imagine the following transformation:

(defun f ( { args } [ &rest rest ] &key { kwargs } [ &allow-other-keys ] )
  ...)

;; - - ->

(defun f ( { args } &rest rest )
  (destructuring-bind ( &key { kwargs } [ &allow-other-keys ] ) rest
    ...))

This might make it a bit more clear.


It is possible for you to make your function behave how you want (not using getf though because of parity) but I would argue that it is wrong. Consider the following:

(defun foobar-sym (k)
  (create-symbol 'foo k 'bar))

CL-USER> (foobar-sym :baz)
#:FOOBAZBAR
CL-USER> (foobar-sym :intern)
FOO

this is slightly weird.

Upvotes: 2

Related Questions