0

I currently have a macro defined as follows:

(defmacro some-macro (generic-name (&rest args) &body body) ...) 

I would now like to add an extra parameter to this macro, a kind of flag which a user can optionally provide and which would alter the behavior of the macro. The below code should be possible:

(some-macro some-name (arg1 arg2) (print (+ arg1 arg2))) (some-macro :flag some-name (arg1 arg2) (print (special-+ arg1 arg2))) 

I intentionally just made a conceptual example to focus on what's important instead of the real macro I'm trying to implement. The real a macro is different and more complex. As you can see, the macro should now be callable with both the :flag argument (flag can be any word, as long as its prefix is ':') and without the flag argument. Is there a way to do this without positioning the &optional keyword at the end of the parameter list (ie. it really needs to be at that first position).

8
  • If you want to do this, you have to do your own argument parsing, you can't use the built-in destructuring. Commented Dec 31, 2019 at 0:56
  • Is :flag supposed to be the generic-name? If not, where is that in your calls? Commented Dec 31, 2019 at 0:56
  • :flag can take one of three froms in my code, let's call it :flag1, :flag2 and :flag3. Depending on the flag, or no flag, the macro is expanded in a specific way. I'm not entirely familiar with generic-name if it is something specific you are refering to, but hope this clears some stuff up. Commented Dec 31, 2019 at 1:02
  • It's the first argument to your macro: (defmacro some-macro (generic-name ...)) Commented Dec 31, 2019 at 1:03
  • If generic-name is a synonym for the first argument of my macro, then yes it is the generic-name. However, it is optional. Providing no flag should work as well. The exact syntax of how the macros should be called is in the second code-block of the question. Commented Dec 31, 2019 at 1:07

4 Answers 4

4

&optional can only be at the end of the positional arguments (you can have &rest or &body after it). Even if you could put it earlier, if there's also &rest or &body, how would it know whether you provided the optional argument? E.g. if the lambda list were

(&optional arg1 arg2 &rest rest-args) 

and the call were

(func-name 1 2 3 4 5) 

it could either be arg1 = 1, arg2 = 2, rest-args = (3 4 5) or arg1 = NIL, arg2 = 1, rest-args = (2 3 4 5).

You'll need to define the macro to take a single &rest argument. Then you can check whether the first argument is a keyword or not, update the argument list to add the default, and then parse it with destructuring-bind.

(defmacro some-macro (&rest all-args) (unless (keywordp (first all-args)) (push nil all-args)) ;; flag defaults to NIL (destructuring-bind (flag generic-name (&rest args) &body body) all-args ...)) 
Sign up to request clarification or add additional context in comments.

2 Comments

Wouldn't you need to pop the :keyword argument to make the all-args have the keyword value as the first element?
The keyword is not being used to introduce another argument, it's the argument all by itself.
2
(some-macro :flag some-name (arg1 arg2) (print (special-+ arg1 arg2))) (some-macro some-name (arg1 arg2) (print (+ arg1 arg2))) 

One can only guess what's useful, since the design of the macro depends a bit on more context: what is it actually used for.

For example in CLOS we write

(defmethod foo :around ((a class-a)) ...) 

The name comes first, then zero or more method-qualifiers (here :around and then the arglist. Putting a flag in front of a name would be strange in typical defining macros (those who begin with def).

For that we would need to write the macro-destructuring of the arglist ourselves, since it does not match a built-in macro arglist pattern.

(defmacro some-macro (name &rest args) (let* ((qualifiers (loop for arg in args until (listp arg) collect (pop args))) (arg-list (pop args)) (body args)) ... ; return something for the example )) 

In other macros we might write

(some-macro some-name (arg1 arg2 :flag) (print (special-+ arg1 arg2))) (defmacro some-macro (some-name (arg1 arg2 &optional flag) &body body) ...) 

Similar for example to

(with-input-from-string (stream string :start 10) (... )) 

Though above uses keywords, not optionals.

Or we might want to write:

(some-macro some-name (arg1 arg2) (:flag1) (print (special-+ arg1 arg2))) (defmacro some-macro (some-name (&rest args) (&rest flags) &body body) ...) 

If there are only three flags one can also generate three different macros with different names and remove the flag.

1 Comment

Would you be able to ellaborate on how you would deal with the first situation (CLOS situation). Now that I think about it, having the name first would actually be more ideal and what I'm trying to achieve closely matches that example (in fact, it's almost exacly what I'm trying to immitate).
1

One thing to think about when designing things like macros (and, remember, when you are designing macros, you are designing a programming language) is how people expect to read the language. If the programming language you are designing is a mild superset of CL, or is otherwise very close to CL, then you may want to not violate the expectations CL programmers have when reading CL code, or more generally that Lisp programmers have when reading Lisp code.

[Note that most of what follows is opinion: people obviously have different opinions – these are mine, they're not more right than anyone else's.]

So, what are those expectations? Well, they may include the following two:

  • people read CL left-to-right, so the things at the left-hand end of a form tend to be more visually important;
  • many existing forms in CL look like (<operator> <thing> ...) – the first two subforms in the form are by far the most interesting, and the second is often more interesting than the first. Think about (defun foo ...), (dolist (x ...) ...), (let ((x y) ...) ...).

An example of something which violates these expectations is an object system which works explicitly with message-passing, using some send operation (I think Old Flavors did this, and I think that New Flavors didn't, but my memory is vague now). Code written using these looks like (send <object> <message> ...): the first word in many forms is send. That means that this visually-important place for reading code has been entirely wasted as it is always the same word and the important places are now the second and third subform. Well, instead, we could just omit the whole send thing and write (message object ...) or (object message ...). CLOS takes essentially the first of these options, where a 'message' is a generic function and of course generic functions can specialise on more than one argument which breaks the whole message-passing paradigm. But you can write CLOS as if it was message-passing, and it works, and it means that it agrees with how a lot of other CL code looks.

So, OK let's look at two cases of your macro:

(some-macro some-name ...) 

This is fine.

(some-macro :flag some-name ...) 

But this has filled the visual second-position slot with something which is not whatever the macro is about: it's just some optional argument. The interesting thing is now the third position.

Well, how could we fix this? It turns out there is a good example which already exists in CL: defstruct. defstruct has two basic cases:

(defstruct structure-name ...) 

and

(defstruct (structure-name ...) ...) 

Both of these meet the first-two-positions-matter-most requirement, while allowing optional arguments and clearly visually indicating when they are used.

(Aside: defclasss does things differently by putting the options at the end as in:

(defclass name (...supers...) (...slot specifications...) ...options...)) 

Either is OK I think.)

So one way to redo your macro would be like defstruct. In this case you would have one of

(some-macro some-name (...) ...) 

or

(some-macro (some-name :flag) (...) ...) 

And you could implement that pretty easily:

(defmacro some-macro (thing (&rest args) &body forms) (multiple-value-bind (the-thing the-options) (etypecase thing (symbol (values thing '())) (cons (destructuring-bind (proposed-name . proposed-options) thing (unless (symbolp proposed-name) (error ...)) (unless (proper-list-p proposed-options) (error ...)) (values proposed-name proposed-options)))) ...)) 

In fact I would go further than this: people expect keyword arguments to have values, because in most other places they do. So instead have

(some-macro (some-name :flag t) (...) ...) 

Which meets that expectation. This has the additional advantage that you can just use CL's argument parsing to get information:

> (destructuring-bind (&key (flag nil flagp)) '(:flag t) (values flag flagp)) t t 

For instance. If you write the macro like this you might end up with something which looks like this:

(defmacro some-macro (thing (&rest args) &body forms) (multiple-value-bind (the-thing flag flagp) (etypecase thing (symbol (values thing nil nil)) (cons (destructuring-bind (proposed-name (&key (flag nil flagp))) thing (unless (symbolp proposed-name) (error ...)) (values proposed-name flag flagp)))) ...)) 

As an aside it's worth considering why defclass & defstruct do things differently, and what this means for other macros.

defstruct looks, in outline, like

(defstruct structure-name-and-options slot-description ...) 

What this means is that, if you put options associated with the structure itself at the end, they're going to get confused with slot descriptions.

defclass gets around this by looking like this:

(defclass class-name (superclass-name ...) (slot-description ...) [class-option] ...) 

It has nested the slot descriptions inside another list, which means that there is now space in the pattern for class options at the end of the form.

For macros which have some kind of 'body', then the natural pattern looks like

(with-foo something [some-more-special-things] ... form ...) 

For example

(with-slots (sa sb) x (when (> sa sb) (setf sb (+ sa sb))) (values sa sb)) 

The problem here is that the whole tail of the macro form is the body which means that there is no natural place for options at the end: the only place to put them is at the beginning somewhere. You could get around this, again, by nesting the body:

(with-weird-thing x (y z) ((when y ...) (print z) ...) option ...) 

But this, again, violates people's expectations: no (?) standard CL macros do this. It's significant that defclass's 'body' is not some forms: it's a list of slot specifications. So it's reasonable to adopt this pattern for defclass.

Finally it's worth considering defmethod. If I had designed this I would have done it slightly differently!

Comments

0

No-nonsense approach: rewrite the macro syntax with flags into another macro which takes a fixed argument containing the flags:

(defmacro some-macro-w-flags (flags name (&rest args) &body body) ...) (defmacro some-macro (&rest args) (let ((flags)) (loop while (keywordp (car args)) do (push (pop args) flags)) `(some-macro-w-flags ,flags ,@args))) 

A few tests:

[1]> (macroexpand-1 '(some-macro abc (1 2 3))) (SOME-MACRO-W-FLAGS NIL ABC (1 2 3)) ; T [2]> (macroexpand-1 '(some-macro :foo abc (1 2 3))) (SOME-MACRO-W-FLAGS (:FOO) ABC (1 2 3)) ; T [3]> (macroexpand-1 '(some-macro :foo :bar abc (1 2 3))) (SOME-MACRO-W-FLAGS (:BAR :FOO) ABC (1 2 3)) ; T 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.