Скажем, я определяю
(defmacro macro-print (str)
(print "hello"))
и беги
(let ((i 0))
(while (< i 3)
(macro-print i)
(setq i (+ 1 i))))
Выход
"hello"
nil
почему это?
Я ожидал увидеть 3 hello
s. Это связано с тем, что, согласно руководству, специальная форма while
сначала оценивает свое состояние, и если она не равна нулю, она оценивает формы в своем теле. Поскольку условие выполняется для трех значений i
, я ожидаю, что форма (macro-print i)
будет оценена 3 раза. Каждый раз, когда он расширяется, он должен печатать «привет». Тем не менее, похоже, что интерпретатор заменяет (macro-print i)
своим расширением (поскольку print возвращает переданную ему строку, он заменит ее на «hello») при первой оценке. Почему это происходит?
Я уже знаю, что правильным способом написания macro-print
будет возврат (list 'print ,i)
, но тот факт, что я не смог предсказать фактическое поведение цикла, указывает на непонимание модели lisp, которую я хочу исправить.
Это связано с тем, что наиболее распространенной стратегией расширения макросов является развертывание всей формы верхнего уровня перед ее оценкой или компиляцией.
Можно продолжать расширять каждое подвыражение каждый раз, когда оно оценивается, и я читал об этой стратегии в некоторых старых материалах по Лиспу. У него есть некоторые преимущества для интерактивного использования: если вы переопределяете макрос, новая версия немедленно вступает в силу для последующих вычислений этого макроса. Эта стратегия расширения кажется совершенно несовместимой с эффективной компиляцией.
Расширение всей формы верхнего уровня сначала выполняется процедурой, называемой макрорасширителем, которая знает, как проходить код аналогично компилятору или интерпретатору. Он понимает все специальные формы и отслеживает определения локальных символов по мере прохождения кода. Вместо того, чтобы компилировать или интерпретировать код, он ищет макросы для замены и возвращает расширенную версию кода, в которой есть только вызовы функций и специальные формы.
Итак, я провел тот же эксперимент в SBCL, и, похоже, он как-то связан с компиляцией и оценкой. Когда я меняю режим оценки на интерпретируемый, я вижу результат, который предсказывал. Разве это не проблема? Может ли код вести себя по-разному в зависимости от того, был ли он скомпилирован или интерпретирован?
Да, могут быть различия между компилируемым и интерпретируемым кодом. Одна из таких ситуаций — когда побочные эффекты помещаются в макросы, как то, что вы видите. Обычно это не проблема, потому что макросы обычно не имеют побочных эффектов. Предполагается, что они преобразуют одно выражение в другое, что в идеале выполняется как чистое вычисление. Макросы, имеющие побочные эффекты, должны быть написаны таким образом, чтобы они не ломались из-за времени, порядка или повторения этих эффектов.
Макросы — это, по сути, функции, домен и диапазон которых — исходный код. В CL они буквально таковы: макрос реализуется функцией, аргументом которой является исходный код (и объект среды) и которая возвращает другой исходный код. Я думаю, что в elisp реализация немного менее явная.
Поскольку такие макросы расширяются (что в CL означает, что функция, реализующая макрос, вызывается) по крайней мере один раз во время оценки кода, чтобы они могли вернуть свое раскрытие. Они могут быть расширены ровно один раз (и с точки зрения эффективности это явно было бы идеально) или более одного раза, но они расширены хотя бы один раз. Это единственная гарантия, которая у вас есть в целом.
Для интерактивного интерпретатора, очевидно, было бы неплохо, если бы макросы расширялись каждый раз, когда изменяется либо код, ссылающийся на макрос, либо когда изменяется сам макрос. Этого можно достичь, развернув макрос как часть интерпретатора, опять же хотя бы один раз.
Вы видите, что интерпретатор elisp в некоторых случаях расширяет макросы ровно один раз.
В случае скомпилированных реализаций оценка представляет собой двухэтапный процесс после чтения исходного кода: исходный код превращается в некоторое скомпилированное представление во время компиляции, и это скомпилированное представление выполняется во время выполнения. Расширение макроса произойдет во время компиляции, а не выполнения.
CL фактически обеспечивает это: минимальная компиляция определена частично, так что
Все вызовы макросов и символьных макросов, появляющиеся в компилируемом исходном коде, расширяются во время компиляции таким образом, что они не будут расширяться снова во время выполнения.
Если вам нужен код, который выполняется во время выполнения, используйте либо функцию, либо, если вам нужен макрос для расширения языка, сделайте этот код частью расширения макроса: частью возвращаемого значения, а не побочным эффектом. расширения его.
Вы написали, что знаете, что правильная форма для определения этого макроса была бы такой:
(defmacro macro-print (s)
`(print "hello"))
Если вы сейчас сделаете:
(dotimes (i 3)
(macro-print i))
он правильно печатает 3 раза "привет"
и возвращает nil
.
(В таких случаях вам следует предпочесть dotimes
вместо конструкции let-while-setq
, поскольку для таких циклов побочных эффектов следует предпочесть do-
циклы - и они более читабельны).
Думаю, что именно получится, будет понятнее, когда попробуешь
(defmacro macro-print (s)
`(print "hello"))
(macroexpand-1 '(macro-print 1))
;; => (print "hello")
Но с вашим:
(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!
Так что пока прежняя версия (macro-print <something>)
расширяется до фрагмента кода (print "hello")
,
последняя версия (которую вы использовали!) (macro-print <something>)
расширяется до простой литральной строки "hello"
.
Итак, в вашем примере, если вы делаете
(dotimes (i 3)
(macro-print i))
Это расширяется до
(dotimes (i 3)
(print "hello"))
Что дает желаемое поведение (печать 3x "hello" на stdoutput),
(dotimes (i 3)
"hello")
Который, конечно, ничего не выводит на стандартный вывод (экран).
Единственный раз, когда он печатает что-то на экране,
когда макрос расширяется один раз.
Следовательно, только один "hello"
один экран.
Итак, ваша ошибка в мышлении заключалась в том, что вы думали
ваш макрос расширяется до (print "hello")
, в то время как на самом деле
просто расширяется до "hello"
- поэтому НЕ видит
Трехкратное "привет" на экране вполне ожидаемо!
И еще:
"hello"
на вашем экране был напечатан не во время выполнения, а во время расширения вашего кода,
поэтому он печатался не во время выполнения цикла, а до этого.
Пока - при правильном определении макроса - ничего печатается на экране во время раскрытия макроса, но во время выполнения цикла «привет» печатается на экране 3 раза, как и ожидалось.
macroexpand-1
Это дает вам одно основное правило для программирования макросов в Лиспе:
Всегда используйте macroexpand-1
всякий раз, когда вы определяете макрос, чтобы понять, во что на самом деле расширяется макрос!
Это также происходит в Common Lisp. Это поведение определено где-то в гиперспецификации? Макросы, похоже, не раскрываются сразу, а когда они выполняются один раз и заменяются их формой расширения.