Макросы не являются функциями.Они генерируют код , они применяются в время компиляции и ничего не знают о значениях времени выполнения.Это означает, что даже если вы напишите это:
(define-syntax-rule (some-macro)
(displayln "hello!"))
(when #f
(some-macro))
… использование some-macro
будет все еще расширенным, и программа будет преобразована в этот:
(when #f
(displayln "hello!"))
Это очень важно понять при работе с макросами: макросы в буквальном смысле являются инструментами замены кода.Когда макрос раскрывается, его использование буквально заменяется кодом, созданным макросом.Это может вызвать проблемы с рекурсивными макросами, поскольку если вы не будете осторожны, расширение макросов никогда не прекратится.Рассмотрим пример программы:
(define-syntax-rule (recursive-macro)
(when #f
(displayln "hello!")
(recursive-macro)))
(recursive-macro)
После одного шага макроразложения использование (recursive-macro)
расширится до следующего:
(when #f
(displayln "hello!")
(recursive-macro))
Теперь, если recursive-macro
была функцией,тот факт, что он появляется внутри формы when
, не имеет значения - он просто никогда не будет выполнен.Но recursive-macro
- это не функция, это макрос, и он будет расширен, независимо от того, что ветвь никогда не будет взята во время выполнения.Это означает, что после второго шага макроразложения программа будет преобразована в следующее:
(when #f
(displayln "hello!")
(when #f
(displayln "hello!")
(recursive-macro)))
Я думаю, вы можете видеть, куда это идет.Вложенные значения recursive-macro
никогда не перестанут расширяться, и программа быстро станет неограниченно большой.
Вполне возможно, что вы не удовлетворены этим.Учитывая, что ветка никогда не будет взята, почему расширитель тупо продолжает расширяться?Ну, recursive-macro
- очень глупый макрос, так как было бы не очень полезно писать макрос, который расширяется до кода, который никогда не будет выполнен.Вместо этого представьте, что мы слегка изменили определение recursive-macro
:
(define-syntax-rule (recursive-macro)
(when (zero? (random 2))
(displayln "hello!")
(recursive-macro)))
Теперь ясно, что расширитель не может знать, сколько раз будут выполняться рекурсивные вызовы, так какПоведение является случайным, и генерируемые случайные числа будут отличаться при каждом выполнении программы.Учитывая, что расширение происходит во время компиляции, а не во время выполнения, для расширителя просто не имеет смысла пытаться учесть информацию времени выполнения.
Это то, что не так с вашим макросом while
.Вы, похоже, ожидаете, что while
будет вести себя как вызов функции, и рекурсивное использование while
в ветке времени выполнения, которая не используется, не будет расширено.Это просто неправда: макросы раскрываются во время компиляции, независимо от информации времени выполнения.В самом деле, вы должны думать о процессе макроразложения как о преобразовании, которое создает программу без каких-либо макросов в качестве выходных данных, которая выполняется только затем.
Имея это в виду, как вы можете это исправить?Что ж, при написании макроса вы должны думать о себе как о крошечном компиляторе: вашему макросу необходимо реализовать свои функциональные возможности путем преобразования входного кода в некоторый код, который выполняет желаемое поведение, полностью , определенное с использованием более простогоязыковые особенности.В этом случае очень простой способ реализовать цикл в Racket - это именованный- let
loop , например:
(let ([x 5])
(let loop ()
(when (> x 0)
(displayln x)
(set! x (- x 1))
(loop))))
Это упрощает реализацию вашего while
конструкция: вам просто нужно написать макрос, который преобразует while
в эквивалент с именем- let
:
(define-syntax-rule (while c body ...)
(let loop ()
(when c
body ...
(loop))))
Это будет делать то, что вы ожидаете.
Конечно,этот макрос не очень идиоматическая ракетка.Ракетчики предпочитают избегать set!
и других форм мутаций, и они просто использовали бы одну из встроенных for
конструкций для написания итерации без присваивания.Тем не менее, это вполне разумно, если вы просто экспериментируете с макросистемой.