Подтвердить что ты не робот

Должны ли макросы иметь побочные эффекты?

Может (или должно) расширение макроса иметь побочные эффекты? Например, вот макрос, который фактически идет и захватывает содержимое веб-страницы во время компиляции:

#lang racket

(require (for-syntax net/url))
(require (for-syntax racket/port))

(define-syntax foo
  (lambda (syntx)
    (datum->syntax #'lex
                   (port->string
                     (get-pure-port
                       (string->url
                         (car (cdr (syntax->datum syntx)))))))))

Тогда я могу сделать (foo "http://www.pointlesssites.com/"), и он будет заменен на "\r\n<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\"\r\n\t <and so on>"

Это хорошая практика или нет? Могу ли я гарантировать, что Racket будет запускать этот код только один раз? Если я добавлю строку (display "running...") в макрос, она будет печататься только один раз, но я бы не хотел обобщать из одного примера...

PS - причина, по которой я спрашиваю, потому что я на самом деле думаю, что иногда это может быть действительно полезно. Например this - это библиотека, которая позволяет загружать (во время компиляции) документ обнаружения из службы Google API Discovery и автоматически создавать обертки для Это. Я думаю, было бы действительно здорово, если бы библиотека на самом деле взяла документ обнаружения из Интернета, а не из локального файла.

Кроме того, чтобы привести пример макроса с различными видами побочных эффектов: однажды я создал макрос, который перевел небольшое подмножество Racket в (eta-расширенное) лямбда-исчисление (которое, конечно же, все еще выполняется в Ракетка). Всякий раз, когда макрос завершает перевод функции, он сохраняет результат в словаре, чтобы последующие вызовы макроса могли использовать определение этой функции в своих собственных переводах.

4b9b3361

Ответ 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

Ответ 2

В общем Lisp функция eval-when позволяет вам решить, когда макрос будет расширен.