В информатике гигиенические макросы — это макросы , расширение которых гарантированно не приведет к случайному захвату идентификаторов . Они являются особенностью таких языков программирования , как Scheme , [1] Dylan , [2] Rust , Nim и Julia . Общая проблема случайного захвата была хорошо известна в сообществе Lisp до появления гигиенических макросов. Авторы макросов использовали бы языковые возможности, которые генерировали бы уникальные идентификаторы (например, gensym) или использовали бы запутанные идентификаторы, чтобы избежать этой проблемы. Гигиенические макросы — это программное решение проблемы захвата, которое интегрировано в макрорасширитель. Термин «гигиена» был придуман в статье Колбекера и др. 1986 года, в которой было представлено гигиеническое макрорасширение, вдохновленное терминологией, используемой в математике. [3]
В языках программирования, имеющих негигиеничные макросистемы, возможно, что существующие переменные связывания будут скрыты от макроса переменными связываниями, которые создаются во время его расширения. В C эту проблему можно проиллюстрировать следующим фрагментом:
#define INCI(i) { int a=0; ++i; } int main ( void ) { int a = 4 , b = 8 ; INCI ( a ); INCI ( b ); printf ( "a теперь %d, b теперь %d \n " , a , b ); return 0 ; }
Выполнение вышеприведенного кода через препроцессор C дает:
int main ( void ) { int a = 4 , b = 8 ; { int a = 0 ; ++ a ; }; { int a = 0 ; ++ b ; }; printf ( "a теперь %d, b теперь %d \n " , a , b ); return 0 ; }
Переменная, a
объявленная в верхней области видимости, затеняется переменной a
в макросе, что вводит новую область видимости . В результате, a
никогда не изменяется при выполнении программы, как показывает вывод скомпилированной программы:
а теперь 4, б теперь 9
Проблема гигиены может выходить за рамки привязок переменных. Рассмотрим этот макрос Common Lisp :
( defmacro my-unless ( condition &body body ) ` ( if ( not , condition ) ( progn ,@ body )))
Хотя в этом макросе нет ссылок на переменные, он предполагает, что символы "if", "not" и "progn" привязаны к их обычным определениям в стандартной библиотеке. Однако, если приведенный выше макрос используется в следующем коде:
( flet (( not ( x ) x )) ( my-unless t ( format t "Это не должно быть напечатано!" )))
Определение «не» было локально изменено, поэтому изменилось и расширение my-unless
.
Однако следует отметить, что для Common Lisp такое поведение запрещено, согласно 11.1.2.1.2 Ограничения на пакет COMMON-LISP для соответствующих программ. Также возможно полностью переопределить функции в любом случае. Некоторые реализации Common Lisp предоставляют блокировки пакетов, чтобы предотвратить изменение определений в пакетах пользователем по ошибке.
Конечно, проблема может возникнуть и для программно-определенных функций аналогичным образом:
( defun пользовательский-оператор ( cond ) ( not cond )) ( defmacro my-unless ( condition &body body ) ` ( if ( user-defined-operator , condition ) ( progn ,@ body ))) ; ... позже ...( flet (( user-defined-operator ( x ) x )) ( my-unless t ( format t "Это не должно быть напечатано!" )))
Место использования переопределяет user-defined-operator
и, следовательно, изменяет поведение макроса.
Проблему гигиены можно решить с помощью обычных макросов, используя несколько альтернативных решений.
Самым простым решением, если во время расширения макроса требуется временное хранилище, является использование необычных имен переменных в макросе в надежде, что те же имена никогда не будут использоваться остальной частью программы.
#define INCI(i) { int INCIa = 0; ++i; } int main ( void ) { int a = 4 , b = 8 ; INCI ( a ); INCI ( b ); printf ( "a теперь %d, b теперь %d \n " , a , b ); return 0 ; }
Пока не создана именованная переменная INCIa
, это решение выдает правильный вывод:
а теперь 5, б теперь 9
Проблема решена для текущей программы, но это решение не является надежным. Переменные, используемые внутри макроса, и переменные в остальной части программы должны быть синхронизированы программистом. В частности, использование макроса INCI
на переменной INCIa
приведет к сбою таким же образом, как и исходный макрос потерпел неудачу на переменной a
.
В некоторых языках программирования возможно создание нового имени переменной или символа и привязка его к временному местоположению. Система обработки языка гарантирует, что это никогда не будет конфликтовать с другим именем или местоположением в среде выполнения. Ответственность за выбор использования этой функции в теле определения макроса остается за программистом. Этот метод использовался в MacLisp , где именованная функция gensym
могла использоваться для создания нового имени символа. Похожие функции (обычно gensym
также именованные) существуют во многих языках типа Lisp, включая широко реализованный стандарт Common Lisp [4] и Elisp .
Хотя создание символов решает проблему затенения переменных, оно не решает напрямую проблему переопределения функций. [5] Однако, gensym
макровозможности и стандартные библиотечные функции достаточны для встраивания гигиеничных макросов в негигиеничный язык. [6]
Это похоже на обфускацию, поскольку одно имя используется несколькими расширениями одного и того же макроса. Однако, в отличие от необычного имени, используется неинтернированный символ времени чтения (обозначенный нотацией #:
), для которого невозможно возникновение вне макроса, аналогично gensym
.
Используя пакеты, такие как Common Lisp, макрос просто использует частный символ из пакета, в котором определен макрос. Символ не будет случайно встречаться в пользовательском коде. Пользовательский код должен был бы достичь внутренней части пакета, используя ::
обозначение двойного двоеточия ( ), чтобы дать себе разрешение на использование частного символа, например cool-macros::secret-sym
. В этот момент вопрос случайного отсутствия гигиены становится спорным. Более того, стандарт ANSI Common Lisp классифицирует переопределение стандартных функций и операторов, глобально или локально, как вызов неопределенного поведения . Такое использование может быть таким образом диагностировано реализацией как ошибочное. Таким образом, система пакетов Lisp обеспечивает жизнеспособное, полное решение проблемы гигиены макросов, которую можно рассматривать как случай конфликта имен.
Например, в примере переопределения программно-определяемой функции my-unless
макрос может находиться в своем собственном пакете, где user-defined-operator
— частный символ в этом пакете. Символ, user-defined-operator
встречающийся в пользовательском коде, будет тогда другим символом, не связанным с тем, который используется в определении макроса my-unless
.
В некоторых языках расширение макроса не обязательно должно соответствовать текстовому коду; вместо расширения до выражения, содержащего символ f
, макрос может создать расширение, содержащее фактический объект, на который ссылается f
. Аналогично, если макросу необходимо использовать локальные переменные или объекты, определенные в пакете макроса, он может расшириться до вызова объекта замыкания, чья охватывающая лексическая среда является средой определения макроса.
Гигиенические макросистемы в таких языках, как Scheme, используют процесс макрорасширения, который сохраняет лексическую область действия всех идентификаторов и предотвращает случайный захват. Это свойство называется ссылочной прозрачностью . В случаях, когда захват желателен, некоторые системы позволяют программисту явно нарушать механизмы гигиены макросистемы.
Например, системы создания Scheme let-syntax
и define-syntax
макросов являются гигиеничными, поэтому следующая реализация 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 "Это не должно быть напечатано!" ) ( newline )))
Гигиенический макропроцессор, отвечающий за преобразование шаблонов входной формы в выходную форму, обнаруживает конфликты символов и разрешает их путем временного изменения имен символов. Основная стратегия заключается в определении привязок в определении макроса и замене этих имен на gensyms, а также в определении свободных переменных в определении макроса и обеспечении поиска этих имен в области определения макроса, а не в области, где использовался макрос.
Макросистемы, которые автоматически обеспечивают соблюдение гигиены, возникли в Scheme. Первоначальный алгоритм KFFD для гигиенической макросистемы был представлен Колбекером в 1986 году. [3] В то время стандартная макросистема не была принята реализациями Scheme. Вскоре после этого в 1987 году Колбекер и Ванд предложили декларативный язык на основе шаблонов для написания макросов, который был предшественником макрообъекта, syntax-rules
принятого стандартом R5RS. [1] [7] Синтаксические замыкания, альтернативный механизм гигиены, были предложены в качестве альтернативы системе Колбекера и др. Боуденом и Ризом в 1988 году. [8] В отличие от алгоритма KFFD, синтаксические замыкания требуют, чтобы программист явно указал разрешение области действия идентификатора. В 1993 году Дибвиг и др. представили макросистему syntax-case
, которая использует альтернативное представление синтаксиса и автоматически поддерживает гигиену. [9] Система syntax-case
может выражать syntax-rules
язык шаблонов как производный макрос. Термин «макросистема» может быть неоднозначным, поскольку в контексте Scheme он может относиться как к конструкции сопоставления с образцом (например, синтаксическим правилам), так и к структуре для представления и манипулирования синтаксисом (например, синтаксическим регистром, синтаксическими замыканиями).
Syntax-rules — это высокоуровневое средство сопоставления шаблонов , которое пытается упростить написание макросов. Однако 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
макросистемы, стандарт схемы R6RS принял макросистему с синтаксисом-регистром. [10] В отличие от 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
макросы могут указывать побочные условия для своих правил сопоставления с образцом через произвольные функции Scheme. В качестве альтернативы, автор макроса может не использовать интерфейс сопоставления с образцом и манипулировать синтаксисом напрямую. Используя datum->syntax
функцию, макросы с синтаксисом-регистром также могут намеренно захватывать идентификаторы, тем самым нарушая гигиену.
Другие макросистемы также были предложены и реализованы для Scheme. Синтаксические замыкания и явное переименование [11] являются двумя альтернативными макросистемами. Обе системы являются системами более низкого уровня, чем синтаксические правила, и оставляют обеспечение гигиены автору макроса. Это отличается от обоих синтаксических правил и синтаксиса, которые автоматически обеспечивают гигиену по умолчанию. Примеры обмена, приведенные выше, показаны здесь с использованием синтаксического замыкания и реализации явного переименования соответственно:
;; синтаксические замыкания ( 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 )))))) ;; явное переименование ( 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 ))))))
Гигиенические макросы обеспечивают безопасность и ссылочную прозрачность за счет того, что преднамеренный захват переменных становится менее простым. Дуг Хойт, автор Let Over Lambda , пишет: [16]
Почти все подходы, принятые для снижения влияния захвата переменных, служат только для уменьшения того, что вы можете сделать с defmacro. Гигиенические макросы в лучшем случае являются защитным ограждением для новичков; в худшем случае они образуют электрический забор, запирая своих жертв в продезинфицированной, безопасной для захвата тюрьме.
— Дуг Хойт
Многие гигиенические макросистемы предлагают аварийные выходы без ущерба для гарантий, которые предоставляет гигиена; например, Racket позволяет вам определять параметры синтаксиса, которые позволяют вам выборочно вводить связанные переменные. Грегг Хендершотт приводит пример в Fear of Macros [17] реализации анафорического оператора if таким образом.