ELisp macro different expansions based on static parameters

71 Views Asked by At

I’m trying to understand if it’s possible/good practice to write macros that expand differently depending on whether the arguments are constants or symbols. As an example (in Emacs 30.0.50):

(defmacro list-length (x)
  (cond (`(symbolp ,x) `(eval (length ,x)))
        (t (length x))))

The different ways I’d expect it to be expanded are as follows:

1.

(let ((x (list 1 2 3 4 5)))
  (macroexpand '(list-length x)))

=> (eval (length x))

OR

2.

(macroexpand '(list-length (1 2 3 4)))

=> 4

A more real-world use case for myself is in something like https://github.com/nealsid/simpleproj/blob/main/util.el#L18, and what I’d like to do in particular is either expand to several calls to puthash if the macro arguments can be determined at expansion time, or otherwise expand to a cl-loop which processes elements of the list passed in at run-time. What exactly “determined at expansion time” means is a bit fuzzy to me (symbolp? Boundp? Or if there is something more I am missing). Thanks!

2

There are 2 best solutions below

0
coredump On BEST ANSWER

The code you quoted follows this structure:

(defun fun (&rest args)
  (loop for (k v) on args by 'cddr do ...))

Let's assume first that you rename the function fun% instead, and define a macro named fun, which at first can be as simple as that:

(defmacro fun (&rest args)
  `(fun% ,@args))

Each time you invoke fun directly, you will know how many parameters you have:

(fun :a 0 :b 1)

what I’d like to do in particular is either expand to several calls to puthash if the macro arguments can be determined at expansion time, or otherwise expand to a cl-loop which processes elements of the list passed in at run-time.

Syntactically, this is a call with 4 arguments, the macro can access it through args variable which is bound to (:a 0 :b 1). So there is no case where the macro doesn't know how many arguments are passed to the call. Only the function fun% can receive an arbitrary number of arguments when you call it using apply:

(let ((list (list :a 0 :b 1)))
  (apply 'fun% list))

But macros cannot be applied like that (you generally don't invoke the macro-function yourself, the macroexpansion mechanism do it for you).

The situation would be different if args was not a &rest argument, if you had instead:

(defmacro fun (args)
  ....)

Here you could only invoke fun with exactly one argument:

(fun <list>)

You have two common situations, both of which allow you to either optimize during expansion or defer to foo%:

  1. You want fun to have the same evaluation mechanism as usual functions, ie. you want the <list> expression to be evaluated as-is at runtime, and then use the value to populate the list. If you do so, then you can optimize for some known cases, like the list being constant (it is a quoted list). You will have to defer to a call to foo% anytime you don't know if args is a constant list or not.

  2. You are defining a special form where standard evaluation rules no longer apply. For example, you decide that the set of keys is probably always the same, and only the values need to be evaluated. You specify your own grammar:

     ARGS ::= SYMBOL | KW-LIST
     KW-LIST := (KEYWORD FORM . KW-LIST) | NIL
    

    KEYWORD items are expected to be literal symbols, but FORM values must be injected in the generated code at the place where their value is needed (in the calls to puthash).

    Valid calls are:

     (foo ())
     (foo (:a 10 :b (+ 10 20) :c (* 5 30)))
     (foo my-args)
    

    Note how the list is not quoted, it is part of the syntax of foo, it can be parsed at runtime and you can know its size, the only unknonw things in the macro are the values associated with the fixed set of keys.

Both are possible, you generally never need to call eval, because how could the compiler ever know what the values are going to be when the code is run, in a totally different environment? Consider this Common Lisp code:

(progn
  (print "Enter a list: ")
  (let ((list (read)))
    (fun list)))

If fun is a macro, you cannot know how many items the list has, this is something that will be known after the user answer the prompt.

3
ignis volens On

This is really a comment, but it's too long. In summary: I think you have some misunderstanding about what Lisp macros do, but I'm not sure what that misunderstanding is from this small example. Here at least is an attempt to show what this macro does, and does not, do.

eval

The first important hint is: if your macro expands to eval you are almost certainly making a mistake. That is not quite always true, but it almost always is true. Using eval is the equivalent of using a tactical nuclear weapon as a hammer. In fact it's worse than that: nuclear weapons have elaborate safety mechanisms and almost never go off by mistake, while eval has none: using eval is like using slightly-subcritical configurations of plutonium as hammers. What's worse, even if the prompt criticality doesn't get you and you don't die from just plain toxicity it probably does not do what you expect in a lexically-scoped lisp (which elisp now can be).

This macro

Here is the macro from the question:

(defmacro list-length (x)
  (cond (`(symbolp ,x) `(eval (length ,x)))
        (t (length x))))

Well, without a statement about what the intent of the thing is, what actually is it?

Well let's just look at the body of the macro and expand it a little:

(cond (`(symbolp ,x) ...)
      (... ...))

Is exactly the same as

(cond ((list 'symbolp x) ...)
      (... ...))

Well, (list 'symbolp ...) is always true, so the first clause of the cond will always be the one selected.

So then we can just replace this entire cond expression with the body of its first clause. So the macro is in fact completely equivalent to this:

(defmacro list-length (x)
  `(eval (length ,x)))

OK, so now let's look at the expansion of this. Well

(list-length <anything>)

expands to

(eval (length <anything>))

And now wait: eval is just a function, so its arguments are evaluated. But length just returns a number. Numbers are self-evaluating. So this is just an extremely expensive way of saying (length <anything>). Which is exactly the same thing as the second clause of the cond if that clause had ever been selected!

What you might be groping towards

As I said, I can't work out what you are confused about, but here's what you might be trying to do. Common Lisp has a function called constantp which can be used in macros (and compiler macros, which elisp doesn't have, but where it's often more useful) to give a partial answer to the question 'will this form be a constant when it's evaluated?' In particular, constantp will return true for

  • self-evaluating objects such as numbers, characters and so on;
  • constant variables, such as keywords, symbols defined by CL to be constant and also symbols declared as constant by the user using defconstant (which elisp does not really have: defconst does not have the strong guarantees around it that defconstant has);
  • any form which looks like (quote <anything>);
  • and perhaps other implementation-defined things.

Well, elisp doesn't have constantp but you could write a partial implementation of it pretty easily. Then you could write macros which expand suitably.

Here is such a list-length macro which will check explicitly for literal strings and lists (or even quoted strings):

(defmacro len (x)
  (cond
   ((stringp x)
    (length x))
   ((and (consp x)
         (eql (car x) 'quote))
    (length (cadr x)))
   (t
     `(length ,x))))

This is still ugly because list-length is conceptually a function, not a macro, and using a macro to do what a function should do is horrid. This is the sort of hack people used to have to do in antique Lisps.

Common Lisp has things called compiler macros which address exactly this problem: you can define macros the compiler will use to simplify functions. So in CL you might write:

(defun len (x)
  (length x))

(define-compiler-macro len (&whole form x)
  (if (constantp x)
      (length (constant-value x))
    form))

Where constant-value is a little function which deals with (quote <x>) properly. The compiler macro checks if the thing is a constant, computes the length at compile time if so, and punts to len otherwise.

It looks like you can in fact now do something like this in elisp using define-inline, although I don't understand it properly. I think this may be a correct definition:

(define-inline len (x)
  (if (inline-const-p x)
      (length (inline-const-val x))
    (inline-quote (length ,x))))

This will define len as a function (not a macro) but its definition will be expanded in the same way the CL compiler macro was, I think.