Гигиенический макрос

редактировать

Гигиенические макросы являются макросы, разложение гарантирована не вызвать случайный захват из идентификаторов. Они являются особенностью таких языков программирования, как Scheme, Dylan, Rust, Nim и Julia. Общая проблема случайного захвата была хорошо известна в Лиспе. сообщества до внедрения гигиенических макросов. Создатели макросов будут использовать языковые функции, которые будут генерировать уникальные идентификаторы (например, gensym), или использовать запутанные идентификаторы, чтобы избежать проблемы. Гигиенические макросы - это программное решение проблемы захвата, встроенное в сам макрорасширитель. Термин «гигиена» был придуман в статье Кольбекера и др. 1986 года, в которой было введено макрорасширение гигиены, вдохновленное терминологией, используемой в математике.

СОДЕРЖАНИЕ
  • 1 Проблема гигиены
  • 2 Стратегии, используемые в языках, в которых отсутствуют гигиенические макросы
  • 3 Реализации
    • 3.1 Языки с гигиеническими макросистемами
  • 4 Критика
  • 5 См. Также
  • 6 Примечания
  • 7 ссылки
Проблема гигиены

В языках программирования, которые имеют негигиеничные макросистемы, существующие привязки переменных могут быть скрыты от макроса привязками переменных, которые создаются во время его расширения. В языке 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, макросы предоставляют мощное средство расширения языка. Здесь недостаток гигиены в обычных макросах решается несколькими стратегиями.

Обфускация
Если во время раскрытия макроса требуется временное хранилище, можно использовать необычные имена переменных в надежде, что те же имена никогда не будут использоваться в программе, использующей макрос.
Создание временного символа
В некоторых языках программирования возможно сгенерировать новое имя переменной или символ и связать их с временным местоположением. Система языковой обработки гарантирует, что это никогда не конфликтует с другим именем или местоположением в среде выполнения. Ответственность за выбор использования этой функции в теле определения макроса возлагается на программиста. Этот метод использовался в MacLisp, где указанная функция 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))))))

Языки с гигиеническими макросистемами

  • Схема - правила синтаксиса, регистр синтаксиса, синтаксические замыкания и другие.
  • Ракетка - ответвление Scheme. Его макросистема изначально была основана на синтаксическом регистре, но теперь имеет больше функций.
  • Nemerle
  • Дилан
  • Эликсир
  • Ним
  • Ржавчина
  • Haxe
  • Mary2 - макро-тела с областью видимости на языке, производном от Algol68, около 1978 г.
  • Юлия
  • Raku - поддерживает как гигиенические, так и антисанитарные макросы
Критика

Гигиенические макросы обеспечивают некоторую безопасность для программиста за счет ограничения возможностей макросов. Как прямое следствие, макросы Common Lisp гораздо более мощные, чем макросы Scheme, с точки зрения того, что с их помощью можно достичь. Дуг Хойт, автор Let Over Lambda, заявил:

Почти все подходы, используемые для уменьшения влияния захвата переменных, служат только для уменьшения того, что вы можете сделать с помощью defmacro. Гигиенические макросы в лучшем случае служат защитным ограждением для новичков; в худшем случае они образуют электрический забор, запирая своих жертв в продезинфицированной, безопасной для задержания тюрьме.

-  Дуг Хойт
Смотрите также
Примечания
использованная литература
Последняя правка сделана 2023-03-27 10:59:00
Содержание доступно по лицензии CC BY-SA 3.0 (если не указано иное).
Обратная связь: support@alphapedia.ru
Соглашение
О проекте