Гигиенические макросы являются макросы, разложение гарантирована не вызвать случайный захват из идентификаторов. Они являются особенностью таких языков программирования, как Scheme, Dylan, Rust, Nim и Julia. Общая проблема случайного захвата была хорошо известна в Лиспе. сообщества до внедрения гигиенических макросов. Создатели макросов будут использовать языковые функции, которые будут генерировать уникальные идентификаторы (например, gensym), или использовать запутанные идентификаторы, чтобы избежать проблемы. Гигиенические макросы - это программное решение проблемы захвата, встроенное в сам макрорасширитель. Термин «гигиена» был придуман в статье Кольбекера и др. 1986 года, в которой было введено макрорасширение гигиены, вдохновленное терминологией, используемой в математике.
В языках программирования, которые имеют негигиеничные макросистемы, существующие привязки переменных могут быть скрыты от макроса привязками переменных, которые создаются во время его расширения. В языке C эту проблему можно проиллюстрировать следующим фрагментом:
#define INCI(i) do { int a=0; ++i; } while (0) int main(void) { int a = 4, b = 8; INCI(a); INCI(b); printf("a is now %d, b is now %d\n", a, b); return 0; }
Выполнение вышеуказанного через препроцессор C дает:
int main(void) { int a = 4, b = 8; do { int a = 0; ++a; } while (0); do { int a = 0; ++b; } while (0); printf("a is now %d, b is now %d\n", a, b); return 0; }
Переменная a
объявлена в верхнем объеме находится в тени с помощью a
переменной в макро, который вводит новую сферу. В результате он никогда не изменяется при выполнении программы, как показывает вывод скомпилированной программы:
a is now 4, b is now 9
Самое простое решение - дать макросам имена переменных, которые не конфликтуют ни с одной переменной в текущей программе:
#define INCI(i) do { int INCIa = 0; ++i; } while (0) int main(void) { int a = 4, b = 8; INCI(a); INCI(b); printf("a is now %d, b is now %d\n", a, b); return 0; }
Пока не будет создана переменная с именем INCIa
, это решение дает правильный результат:
a is now 5, b is now 9
Проблема решена для текущей программы, но это решение не является надежным. Переменные, используемые внутри макроса и в остальной части программы, должны синхронизироваться программистом. В частности, использование макроса INCI
для переменной INCIa
приведет к ошибке так же, как исходный макрос для переменной a
.
«Гигиеническая проблема» может выходить за рамки привязки переменных. Рассмотрим этот макрос Common Lisp :
(defmacro my-unless (condition amp;body body) `(if (not,condition) (progn,@body)))
Хотя в этом макросе нет ссылок на переменные, предполагается, что символы «if», «not» и «progn» связаны со своими обычными определениями. Если, однако, вышеуказанный макрос используется в следующем коде:
(flet ((not (x) x)) (my-unless t (format t "This should not be printed!")))
Определение «не» было изменено локально, и, следовательно, расширение my-unless
изменений. (Переопределение стандартных функций и операторов, глобально или локально, фактически вызывает неопределенное поведение в соответствии с ANSI Common Lisp. Такое использование может быть диагностировано реализацией как ошибочное.)
С другой стороны, гигиенические макросистемы автоматически сохраняют лексическую область видимости всех идентификаторов (таких как «если» и «не»). Это свойство называется ссылочной прозрачностью.
Конечно, проблема может возникнуть для программно определенных функций, которые не защищены таким же образом:
(defmacro my-unless (condition amp;body body) `(if (user-defined-operator,condition) (progn,@body))) (flet ((user-defined-operator (x) x)) (my-unless t (format t "This should not be printed!")))
Решение этой проблемы в Common Lisp - использование пакетов. my-unless
Макрос может находиться в отдельном пакете, где user-defined-operator
находится частный символ в этом пакете. Тогда символ, user-defined-operator
встречающийся в пользовательском коде, будет другим символом, не связанным с тем, который используется в определении my-unless
макроса.
Между тем, такие языки, как Scheme, которые используют гигиенические макросы, предотвращают случайный захват и автоматически обеспечивают ссылочную прозрачность как часть процесса расширения макроса. В случаях, когда требуется захват, некоторые системы позволяют программисту явно нарушать гигиенические механизмы макросистемы.
Например, следующая реализация схемы my-unless
будет иметь желаемое поведение:
(define-syntax my-unless (syntax-rules () ((_ condition body...) (if (not condition) (begin body...))))) (let ((not (lambda (x) x))) (my-unless #t (display "This should not be printed!") (newline)))
В некоторых языках, таких как Common Lisp, Scheme и других из семейства языков Lisp, макросы предоставляют мощное средство расширения языка. Здесь недостаток гигиены в обычных макросах решается несколькими стратегиями.
gensym
могла использоваться для генерации нового имени символа. Подобные функции (также обычно называемые gensym
) существуют во многих Lisp-подобных языках, включая широко применяемый стандарт Common Lisp и Elisp.#:
нотацией), для которого он невозможен вне макроса.::
нотацию с двойным двоеточием (), например, чтобы дать себе разрешение на использование частного символа cool-macros::secret-sym
. В этот момент вопрос о случайном несоблюдении гигиены остается спорным. Таким образом, система пакетов Lisp обеспечивает жизнеспособное, полное решение проблемы макросигиены, которую можно рассматривать как пример конфликта имен.let-syntax
и define-syntax
макросов. Основная стратегия состоит в том, чтобы идентифицировать привязки в определении макроса и заменять эти имена на gensyms, а также идентифицировать свободные переменные в определении макроса и следить за тем, чтобы эти имена просматривались в области определения макроса, а не в области, в которой макрос был использовал.f
, макрос может производить раскрытие, содержащее фактический объект, на который ссылается f
. Точно так же, если макросу необходимо использовать локальные переменные или объекты, определенные в пакете макроса, он может расширяться до вызова закрывающего объекта, чья включающая лексическая среда соответствует определению макроса.Макросистемы, которые автоматически обеспечивают соблюдение гигиены, возникли в Scheme. Первоначальный алгоритм ( алгоритм KFFD) для гигиенической макросистемы был представлен Кольбекером в 1986 году. В то время реализация Scheme не применяла стандартной макросистемы. Вскоре после этого, в 1987 году, Кольбекер и Ванд предложили декларативный язык, основанный на шаблонах, для написания макросов, который был предшественником syntax-rules
макросов, принятых в стандарте R5RS. Синтаксические замыкания, альтернативный гигиенический механизм, были предложены Боуденом и Рисом в 1988 году в качестве альтернативы системе Кольбекера и др. В отличие от алгоритма KFFD, синтаксические замыкания требуют, чтобы программист явно указал разрешение области действия идентификатора. В 1993 году Дибвиг и др. представила syntax-case
макросистему, которая использует альтернативное представление синтаксиса и автоматически поддерживает гигиену. syntax-case
Система может выразить syntax-rules
язык шаблонов в качестве производной макрокоманды.
Термин макросистема может быть неоднозначным, потому что в контексте схемы он может относиться как к конструкции сопоставления с образцом (например, правила синтаксиса), так и к структуре для представления синтаксиса и управления им (например, case-case, синтаксические замыкания).. Syntax-rules - это высокоуровневое средство сопоставления с образцом, которое пытается упростить написание макросов. Однако syntax-rules
не может кратко описать определенные классы макросов и недостаточен для выражения других макросистем. Правила синтаксиса описаны в документе R4RS в приложении, но не обязательны. Позже R5RS принял его как стандартное средство макроса. Вот пример syntax-rules
макроса, который меняет местами значения двух переменных:
(define-syntax swap! (syntax-rules () ((_ a b) (let ((temp a)) (set! a b) (set! b temp)))))
Из-за недостатков чисто syntax-rules
основанной макросистемы низкоуровневые макросистемы также были предложены и реализованы для Scheme. Syntax-case - одна из таких систем. В отличие от syntax-rules
, syntax-case
содержит как язык сопоставления с образцом, так и средство низкого уровня для написания макросов. Первый позволяет писать макросы декларативно, а второй позволяет реализовать альтернативные интерфейсы для написания макросов. Предыдущий пример подкачки почти идентичен, syntax-case
потому что похож на язык сопоставления с образцом:
(define-syntax swap! (lambda (stx) (syntax-case stx () ((_ a b) (syntax (let ((temp a)) (set! a b) (set! b temp)))))))
Однако syntax-case
это более мощный инструмент, чем правила синтаксиса. Например, syntax-case
макросы могут указывать побочные условия в правилах сопоставления с образцом с помощью произвольных функций схемы. В качестве альтернативы, разработчик макросов может отказаться от использования интерфейса сопоставления с образцом и напрямую манипулировать синтаксисом. Используя эту datum-gt;syntax
функцию, макросы синтаксического регистра также могут намеренно захватывать идентификаторы, нарушая тем самым гигиену. R6RS стандартная схема принята система макросов синтаксиса дела.
Синтаксические замыкания и явное переименование - две другие альтернативные макросистемы. Обе системы являются более низкоуровневыми, чем правила синтаксиса, и оставляют соблюдение гигиены на усмотрение автора макроса. Это отличается как от синтаксических правил, так и от синтаксиса, которые по умолчанию автоматически обеспечивают соблюдение гигиены. Приведенные выше примеры подкачки показаны здесь с использованием синтаксического закрытия и реализации явного переименования соответственно:
;; syntactic closures (define-syntax swap! (sc-macro-transformer (lambda (form environment) (let ((a (close-syntax (cadr form) environment)) (b (close-syntax (caddr form) environment))) `(let ((temp,a)) (set!,a,b) (set!,b temp)))))) ;; explicit renaming (define-syntax swap! (er-macro-transformer (lambda (form rename compare) (let ((a (cadr form)) (b (caddr form)) (temp (rename 'temp))) `(,(rename 'let) ((,temp,a)) (,(rename 'set!),a,b) (,(rename 'set!),b,temp))))))
Гигиенические макросы обеспечивают некоторую безопасность для программиста за счет ограничения возможностей макросов. Как прямое следствие, макросы Common Lisp гораздо более мощные, чем макросы Scheme, с точки зрения того, что с их помощью можно достичь. Дуг Хойт, автор Let Over Lambda, заявил:
Почти все подходы, используемые для уменьшения влияния захвата переменных, служат только для уменьшения того, что вы можете сделать с помощью defmacro. Гигиенические макросы в лучшем случае служат защитным ограждением для новичков; в худшем случае они образуют электрический забор, запирая своих жертв в продезинфицированной, безопасной для задержания тюрьме.
- Дуг Хойт