Ответ 1
Короткий ответ
Хорошо, что макросы имеют побочные эффекты, но вы должны убедиться, что ваша программа не меняет поведения, когда она компилируется раньше времени.
Более длинный ответ
Макросы с побочными эффектами являются мощным инструментом и могут позволить вам делать то, что делает программы намного легче писать или позволять вещи, которые были бы невозможны вообще. Но есть недостатки, о которых следует знать, когда вы используете побочные эффекты в макросах. К счастью, Racket предоставляет все инструменты, чтобы убедиться, что вы можете сделать это правильно.
Самый простой вид макро-эффекта - это то, где вы используете какое-то внешнее состояние, чтобы найти код, который хотите сгенерировать. Примеры, которые вы перечисляете в вопросе (чтение описания API Google), относятся к этому типу. Еще более простым примером является макрос include
:
#lang racket
(include "my-file.rktl")
Это считывает содержимое myfile.rktl
и оставляет его на месте, где используется форма include
.
Теперь include
не является хорошим способом структурировать вашу программу, но это довольно мягкий вид побочного эффекта в макросе. Он работает так же, если вы заранее скопируете файл, как если бы вы этого не сделали, поскольку результат include
является частью файла.
Еще один простой пример: "Нехорошо" - это что-то вроде этого:
#lang racket
(define-syntax (show-file stx)
(printf "using file ~a\n" (syntax-source stx))
#'(void))
(show-file)
Это потому, что printf
выполняется только во время компиляции, поэтому, если вы скомпилируете свою программу, которая использует show-file
раньше времени (как и в raco make
), тогда произойдет printf
, и не будет происходят, когда программа запускается, что, вероятно, не является намерением.
К счастью, у Racket есть метод, позволяющий эффективно писать макросы, такие как show-file
. Основная идея - оставить остаточный код, который фактически выполняет побочный эффект. В частности, для этой цели вы можете использовать форму Racket begin-for-syntax
. Вот как я писал show-file
:
#lang racket
(define-syntax (show-file stx)
#`(begin-for-syntax
(printf "using file ~a\n" #,(syntax-source stx))))
(show-file)
Теперь, вместо того, чтобы происходить при расширении макроса show-file
, printf
происходит в коде, который генерируется show-file
, с источником, встроенным в расширенный синтаксис. Таким образом, ваша программа будет работать правильно при наличии компиляции в режиме времени.
Существуют и другие применения макросов с побочными эффектами. Один из самых известных в Racket - это межмодульная связь, потому что require
не дает значений, которые может получить модуль, для связи между модулями наиболее эффективным способом является использование побочных эффектов. Для выполнения этой работы при наличии компиляции требуется почти то же трюк с begin-for-syntax
.
Это тема, которую сообщество Racket и я, в частности, много думали, и есть несколько академических работ, в которых говорится о том, как это работает:
Составляемые и скомпилированные макросы: вы хотите, когда?, Matthew Flatt, ICFP 2002
Расширенная макрология и реализация типизированной схемы, Райан Калпеппер, Сэм Тобин-Хохштадт и Мэтью Флатт, Семинар по схемам 2007
Языки как библиотеки, Сэм Тобин-Хохштадт, Райан Калпеппер, Винсент Сент-Амур, Мэтью Флатт и Маттиас Феллеисен, PLDI 2011