3

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))))
3
  • Regarding your function, what is (create-symbol 'a :intern t)? How about (create-symbol :a :intern t)? How about (create-symbol :package :intern t)? Commented Mar 14, 2019 at 16:35
  • Right. Back to the drawing board. (Thanks.) Could a macro help out here? Commented Mar 14, 2019 at 17:17
  • After reconsidering, I think I'll just go back to a standard lambda list and make the first argument a required object or list of objects. Good lesson about the complexities of lambda lists! Commented Mar 15, 2019 at 18:01

2 Answers 2

2

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
Sign up to request clarification or add additional context in comments.

1 Comment

Nice discussion. Seibel’s comment in the section you mention also says: “You can safely combine &rest and &key parameters, but the behavior may be a bit surprising initially.” However, it looks like his example at the end doesn’t really work, except for the arguments he provides.
2

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.

2 Comments

As you suggest, maybe the best way to do this is to parse the arguments myself, using just &rest. I’d like to see various ways to do this (keeping keywords last) if you and Jonathan Johansen don't mind editing your answers.
@davypough I do not claim it is the best way, or even possibly the best way. I merely claimed it was possible. I won’t implement it for you.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.