3

Let's say I define

(defmacro macro-print (str)
  (print "hello"))

and run

(let ((i 0))
  (while (< i 3)
    (macro-print i)
    (setq i (+ 1 i))))

The output is

"hello"
nil

why is this?

I was expecting to see 3 hellos. This is because, according to the manual, the while special form first evaluates its condition, if it evaluate to non-nil, it evaluates the forms in its body. Since the condition holds for three values of i, I expect to have the (macro-print i) form evaluated 3 times. Every time it expands, it should print "hello". Yet, it seems like the interpreter replaces (macro-print i) with its expansion (since print returns the string passed to it, it would replace it with "hello") upon first evaluation. Why is this happening?

I already know that the correct way to write macro-print would be to return (list 'print ,i), but the fact I was not able to predict the actual behavior of the loop points to a misunderstanding of the lisp model I want to correct.

Isabella
  • 167
  • 3

3 Answers3

6

Macros are essentially functions whose domain and range is source code. In CL they are quite literally this: a macro is implemented by a function whose argument is source code (and an environment object) and which returns other source code. I think that in elisp the implementation is a little less explicit.

As such macros get expanded (which, in CL, means that the function implementing the macro is called) at least once during evaluation of code, in order that they can return their expansion. They may be expanded exactly once (and, in terms of efficiency, this would clearly be ideal), or more than once, but they are expanded at least once. That's the only guarantee you have in general.

For an interactive interpreter it would obviously be nice if macros get expanded every time either code referring to the macro is changed or when the macro itself is changed. That can be achieved by expanding macros as part of the interpreter, again at least once.

What you are seeing is that the elisp interpreter expands macros exactly once in some cases.

In the case of compiled implementations, evaluation is a two-stage process after source code is read: source code is turned into some compiled representation at compile time, and that compiled representation is executed at run time. Macro expansion will happen during the compilation, not the execution.

CL enforces this in fact: minimal compilation is defined, in part, so that

All macro and symbol macro calls appearing in the source code being compiled are expanded at compile time in such a way that they will not be expanded again at run time.

If you want code that executes at run time then use either a function or, if you need a macro to extend the language, make that code be part of the expansion of the macro: part of the value it returns, not a side-effect of expanding it.

ignis volens
  • 7,040
  • 2
  • 12
3

This is because the most common macro expansion strategy is to expand the entire top-level form before evaluating or compiling it.

It's possible to keep expanding every sub-expression every time it is evaluated, and I've read about that strategy in some old Lisp materials. It has some advantages for interactive use, in that if you redefine a macro, the new version instantly comes into effect for subsequent evaluations of that macro. This expansion strategy seems entirely at odds with efficient compilation.

Expanding the entire top-level form first is performed by a procedure called the macro expander, which knows how to walk the code similarly to a compiler or interpreter. It understands all of the special forms, and keeps track of definitions of local symbols as it recurses through the code. Instead of compiling or interpreting the code, it looks for macros to replace, and returns an expanded version of the code in which there are only function calls and special forms.

Kaz
  • 55,781
  • 9
  • 100
  • 149
  • This also seems to happen in Common Lisp. Is this behavior defined somewhere in the hyperspec? Macros do not seem to be expanded immediately, but when they execute once and are replaced by their expansion form. – Isabella Nov 27 '22 at 05:51
  • So I ran the same experiment in SBCL and it does seem to have something to do with compilation vs evaluation. When I change the evaluator mode to interpreted, I see the output I was predicting. Isn't this a problem? Can code behave differently depending on whether it was compiled or interpreted? – Isabella Nov 27 '22 at 09:10
  • Yes, there can be differences between compiled and interpreted code. One such situation is when side effects are put into macros, like what you're seeing. This is usually not a problem because macros don't usually have side effects. They are supposed to transform one expression into another which is ideally done as a pure calculation. Macros which have side effects have to be written such that they don't break due to the timing, order or repetition of those effects. – Kaz Nov 27 '22 at 10:01
1

You wrote you know that the correct form to define this macro would have been:

(defmacro macro-print (s)
  `(print "hello"))

If you now do:

(dotimes (i 3)
  (macro-print i))

it correctly prints 3 times "hello" and returns nil.

(You should prefer in such cases dotimes instead of the let-while-setq construct, since for such side-effect loops do-loops should be preferred - and are more readable).

I think, what exactly happens will be clearer, when you try

(defmacro macro-print (s)
  `(print "hello"))
(macroexpand-1 '(macro-print 1))
;; => (print "hello")

But with your:

(defmacro macro-print (s)
  (print "hello"))

(macroexpand-1 '(macro-print 1))
;; "hello"     ;; while expanding, `(print "hello")` gets executed
;; => "hello"  ;; this macro expands to the return value
;;             ;; of (print "hello") executed during 
;;             ;; the macroexpansion!

So while the former version of (macro-print <something>) expands to the code snippet (print "hello"), the latter version (which you used!) of (macro-print <something>) expands to a simple litral string "hello".

So, in your example, if you do

(dotimes (i 3)
  (macro-print i))

This expands to

(dotimes (i 3)
  (print "hello"))

Which gives the desired behavior (printing 3x "hello" on the stdoutput),

  • but your actual example expands to:
(dotimes (i 3)
  "hello")

Which of course doesn't print anything to stdout (the screen).

The only time it prints something to the screen is, when the macro gets expanded once. Hence just one "hello" one the screen.

So your mistake in thinking was that you thought your macro expands to (print "hello") while it actually merely expands to "hello" - therefore NOT seeing 3x a "hello" on the screen is totally to be expected!

And another thing: The "hello" on your screen was printed not during execution time but during expansion time of your code, so it was not printed while the loop was being executed, but before that.

While - with the correct definition of the macro - nothing is printed on the screen during expansion of the macro, but during the loop being executed, the "hello" gets printed on the screen 3x as expected.

When dealing with macro's always check with macroexpand-1

This gives you one basic rule for macro programming in Lisp: Always use macroexpand-1 whenever you define a macro - to understand what the macro actually expands to!

Gwang-Jin Kim
  • 9,303
  • 17
  • 30