0

I'm trying to use cl-defmacro, with the supposed benefit of having optional arguments specified with keywords. However, I'm not getting the outcome I would expect.

(cl-defmacro asd (a &rest body &key b)
  (format "%s %s %s" a b body))

(asd 1 :b 2 "qwe")

Results in (error "Keyword argument qwe not one of (:b)"). I.e. the forms supplied to the macro are validated against the keyword arguments, for some obscure reason. Shuffling the arguments around as (asd 1 "qwe" :b 2) doesn't change anything.

I use the workaround of specifying &allow-other-keys, as used in some examples:

(cl-defmacro asd (a &rest body &key b &allow-other-keys)
  (format "%s %s %s" a b body))

(asd 1 :b 2 "qwe")

=> 1 2 (:b 2 qwe)

This time, the keyword arguments are eaten as part of the macro-wrapped expressions (‘body’), even though they're already collected as keyword arguments.

None of this makes sense.

I've googled far and wide and looked through code in Emacs and Doom, but apparently I'm the only one having this problem. Personally I would consider this a bug in cl-lib, though idk how it works in Common Lisp proper.

aaa
  • 426
  • 3
  • 9
  • "*idk how it works in Common Lisp proper.*" --- So WHY don't you ask a question about Common Lisp in Stack Overflow? – shynur Jun 16 '23 at 09:43

2 Answers2

2

While not very intuitive, the error in the first value is expected.

Per the manual ((info "(cl) Argument Lists) inside of Emacs):

The third section consists of a single rest argument. If more arguments were passed to the function than are accounted for by the required and optional arguments, those extra arguments are collected into a list and bound to the “rest” argument variable.

TL;DR "sections": required arguments are first, &optional arguments are second, &rest is third, &key is fourth. We don't need to worry about &aux here.

&key being distinct from required and &optional is significant—keyword arguments will be collected by &rest/&body because they fall within the "extra arguments" mentioned in the manual. Without &allow-other-keys, the remaining arguments must all be keyword arguments and must match those that are specified in the argument list.

Given the first definition, the following works:

(asd 1 :b 2)

The second definition works with arbitrary values because of &allow-other-keys, but, as you've noticed, it doesn't separate the defined &key arguments from the &rest argument.

Some approaches you may consider:

  1. Put your keyword arguments into a destructured list:

    (cl-defmacro asd-destructured (a (&key b) &rest body)
      (format "%s %s %s" a b body))
    

    Then, consumers of your macro can supply keyword options like this:

    (asd-destructured 1 (:b 2) "qwe") ; with :b -- outputs "1 2 (qwe)"
    (asd-destructured 1 () "qwe") ; without :b -- outputs "1 nil (qwe)"
    
  2. Parse and validate keyword arguments from the &rest argument manually. This is done for a lot of Doom's macros. cl-defmacro and &key+&allow-other-keys, could potentially make this easier.

schschsch
  • 21
  • 2
  • Thanks! I would think that calling the macro would split the arguments for me, seeing as they are already parsed for the keyword parameters. But it turns out that this behavior mirrors CL, which has explicit rules that say to put the keyword args into the rest-arg. I posted my findings as a separate answer, which I think is regrettably more definitive on this topic, so marked as ‘accepted’. Nevertheless, I'm grateful for the suggestions and for mentioning Doom's macros. – aaa Jun 30 '23 at 09:40
0

Did some investigation as to how this works in CL. Turns out it's even worse, because not only CL shoves the keyword arguments in the &rest argument (in functions as well as in macros), but it also requires the number of rest-arguments to be even, if the &key word is present in the declaration:

(defmacro asd (a &rest body &key b &allow-other-keys)
  (format t "~s ~s ~s" a b body))

(asd 1 :b 2 (progn))

— results in Error while parsing arguments to ASD DEFMACRO: odd number of elements in keyword/value list.

So the caller would have to always pad the key-and-rest argument list to an even number:

(asd 1 :b 2 (progn) (progn))

=> 1 2 (:B 2 (PROGN) (PROGN))

Which is kinda hilarious, frankly.

Also, since the same rules apply to functions, cl-defun in Elisp suffers from this ailment too, just without the evenness:

(cl-defun asd (a &rest x &key b &allow-other-keys)
  (format "%s %s %s" a b x))

(asd 1 :b 2 "qwe")

=> "1 2 (:b 2 qwe)"

So yeah, as @schschsch wrote, there's no way around filtering the keyword arguments from the rest-argument, if one wants the keyword arguments to be optional. This seems to be one of those 60s-70s quirks forever embedded in the language.

aaa
  • 426
  • 3
  • 9