简体   繁体   中英

Broken While Loop Using Macros in Racket

I am implementing a while loop using Racket macros. My question is, can somebody please explain to me why the following code produces an infinite macro expansion loop? The last statement just before the recursive while call - body - decrements the value of x by 1, so you would think that we will be marching towards the condition where x will become equal to 0. But I'm obviously missing something. Thank you in advance!

(let ([x 5])
(while (> x 0)
     (displayln x)
     (set! x (- x 1))))    

 (define-syntax while
  (syntax-rules ()
    ((while c var body)
        (if
           c
          (begin
           var
           body
          (while c var  body))
          (void)))))

Macros are not functions. They generate code , they apply at compile-time , and they know nothing about runtime values. This means that even if you write this:

(define-syntax-rule (some-macro)
  (displayln "hello!"))

(when #f
  (some-macro))

…the use of some-macro will still be expanded, and the program will be transformed to this one:

(when #f
  (displayln "hello!"))

This is really important to understand when working with macros: macros are literally code replacement tools. When a macro is expanded, the use of the macro is literally replaced with the code produced by the macro. This can cause problems with recursive macros, since if you're not careful, macroexpansion will never terminate. Consider this example program:

(define-syntax-rule (recursive-macro)
  (when #f
    (displayln "hello!")
    (recursive-macro)))

(recursive-macro)

After a single macroexpansion step, the use of (recursive-macro) will expand to this:

(when #f
  (displayln "hello!")
  (recursive-macro))

Now, if recursive-macro were a function, the fact that it appears inside the when form would not matter—it would simply never be executed. But recursive-macro isn't a function, it's a macro, and it will be expanded regardless of the fact that the branch will never be taken at runtime. This means that after a second macroexpansion step, the program will be transformed to this:

(when #f
  (displayln "hello!")
  (when #f
    (displayln "hello!")
    (recursive-macro)))

I think you can see where this is going. The nested recursive-macro uses will never stop expanding, and the program will quickly grow unboundedly large.


It's possible that you are unsatisfied by this. Given that the branch will never be taken, why does the expander stupidly continue expanding? Well, recursive-macro is a very silly macro, as it wouldn't be very useful to ever write a macro that expands to code that will never be executed. Instead, imagine we modified the definition of recursive-macro slightly:

(define-syntax-rule (recursive-macro)
  (when (zero? (random 2))
    (displayln "hello!")
    (recursive-macro)))

Now it's clear that the expander can't know how many times the recursive calls will be executed, since the behavior is random, and the random numbers generated will be different on every program execution. Given that expansion is happening at compile-time, not at runtime, it just doesn't make sense for the expander to try and account for runtime information.

This is what's wrong with your while macro. You seem to be expecting that while will behave like a function call, and a recursive use of while in a runtime branch that isn't taken won't be expanded. This is just not true: macros are expanded at compile-time, regardless of runtime information. Indeed, you should think of the macroexpansion process as a transformation that produces a program without any macros in it as output, which is only then executed.


With that in mind, how can you fix it? Well, when writing a macro, you need to think of yourself as writing a tiny compiler: your macro needs to implement its functionality by transforming the input code into some code that performs the desired behavior, entirely defined using simpler language features. In this case, a very simple way to implement a loop in Racket is a named- let loop , like this one:

(let ([x 5])
  (let loop ()
    (when (> x 0)
      (displayln x)
      (set! x (- x 1))
      (loop))))

This makes it easy to implement your while construct: you just need to write a macro that transforms while into the equivalent named- let :

(define-syntax-rule (while c body ...)
  (let loop ()
    (when c
      body ...
      (loop))))

This will do what you expect.

Of course, this macro isn't very idiomatic Racket. Racketeers prefer to avoid set! and other forms of mutation, and they'd just use one of the built-in for constructs to write iteration without assignment. Still, it's perfectly reasonable if you're just experimenting with the macro system.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM