Сломанный цикл с использованием макросов в Racket - PullRequest
0 голосов
/ 16 октября 2018

Я реализую цикл while с помощью макросов Racket.Мой вопрос, может кто-нибудь объяснить мне, почему следующий код создает бесконечный цикл раскрытия макроса?Последнее утверждение перед рекурсивным вызовом while - body - уменьшает значение x на 1, так что вы можете подумать, что мы пойдем к условию, когда x станет равным 0. Но я явно что-то упускаю.Заранее спасибо!

(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)))))

1 Ответ

0 голосов
/ 16 октября 2018

Макросы не являются функциями.Они генерируют код , они применяются в время компиляции и ничего не знают о значениях времени выполнения.Это означает, что даже если вы напишите это:

(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 конструкций для написания итерации без присваивания.Тем не менее, это вполне разумно, если вы просто экспериментируете с макросистемой.

...