Hygienické makro - Hygienic macro
Hygienické maker jsou makra , jejichž expanze je zaručeno, aby nezpůsobil náhodnému zachycení a identifikátorů . Jsou rysem programovacích jazyků, jako jsou Scheme , Dylan , Rust , Nim a Julia . Obecný problém náhodného zachycení byl v komunitě Lisp dobře znám před zavedením hygienických maker. Spisovatelé maker by používali jazykové funkce, které by generovaly jedinečné identifikátory (např. Gensym), nebo by používali zmatené identifikátory, aby se tomuto problému vyhnuli. Hygienická makra jsou programovým řešením problému se zachycením, který je integrován do samotného makro expandéru. Termín „hygiena“ byl vytvořen v článku Kohlbeckera a kol. Z roku 1986, který zavedl hygienickou makro expanzi, inspirovanou terminologií používanou v matematice.
Problém s hygienou
V programovacích jazycích, které mají nehygienické makrosystémy, je možné, že existující variabilní vazby budou před makrem skryty pomocí variabilních vazeb, které jsou vytvořeny během jejího rozšiřování. V jazyce C lze tento problém znázornit následujícím fragmentem:
#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;
}
Spuštěním výše uvedeného preprocesoru C získáte :
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;
}
Proměnná adeklarována v horním rozsahu je sledován pomocí aproměnné v makru, který zavádí nový rozsah . V důsledku toho se při provádění programu nikdy nezmění, jak ukazuje výstup zkompilovaného programu:
a is now 4, b is now 9
Nejjednodušším řešením je zadat názvy proměnných maker, které nejsou v konfliktu s žádnou proměnnou v aktuálním programu:
#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;
}
Dokud INCIanení vytvořena proměnná s názvem, vytváří toto řešení správný výstup:
a is now 5, b is now 9
Problém je pro aktuální program vyřešen, ale toto řešení není robustní. Proměnné použité uvnitř makra a ve zbytku programu musí programátor udržovat synchronizované. Konkrétně použití makra INCIna proměnné INCIaselže stejným způsobem jako původní makro na proměnné selhalo a.
„Hygienický problém“ může přesahovat variabilní vazby. Zvažte toto běžné makro Lisp :
(defmacro my-unless (condition &body body)
`(if (not ,condition)
(progn
,@body)))
I když v tomto makru nejsou žádné odkazy na proměnné, předpokládá to, že symboly „if“, „not“ a „progn“ jsou vázány na jejich obvyklé definice. Pokud se však výše uvedené makro použije v následujícím kódu:
(flet ((not (x) x))
(my-unless t
(format t "This should not be printed!")))
Definice „ne“ byla místně změněna, a proto došlo k rozšíření my-unlesszměn. (Předefinování standardních funkcí a operátorů, globálně nebo lokálně, ve skutečnosti vyvolává nedefinované chování podle ANSI Common Lisp. Takové použití může být diagnostikováno implementací jako chybné.)
Na druhou stranu hygienické makro systémy automaticky zachovávají lexikální rozsah všech identifikátorů (například „pokud“ a „ne“). Tato vlastnost se nazývá referenční transparentnost .
Problém samozřejmě může nastat u funkcí definovaných programem, které nejsou chráněny stejným způsobem:
(defmacro my-unless (condition &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!")))
Společným řešením Lisp tohoto problému je použití balíčků. my-unlessMakro může pobývat ve vlastním obalu, kde user-defined-operatorje soukromá symbol v tomto balíčku. Symbol user-defined-operatorvyskytující se v uživatelském kódu pak bude jiný symbol, který nesouvisí se symbolem použitým v definici my-unlessmakra.
Mezitím jazyky jako Scheme, které používají hygienická makra, zabraňují náhodnému zachycení a automaticky zajišťují referenční transparentnost v rámci procesu rozšíření makra. V případech, kdy je požadováno zachycení, některé systémy umožňují programátorovi výslovně porušit hygienické mechanismy makro systému.
Například následující implementace schématu my-unlessbude mít požadované chování:
(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)))
Strategie používané v jazycích, které postrádají hygienická makra
V některých jazycích, jako je Common Lisp , Scheme a další z jazykové rodiny Lisp , poskytují makra účinný prostředek k rozšíření jazyka. Zde je nedostatek hygieny v konvenčních makrech vyřešen několika strategiemi.
- Zmatení
- Pokud je při rozšiřování makra potřeba dočasné úložiště, lze použít neobvyklé názvy proměnných v naději, že stejná jména nebudou nikdy použita v programu, který makro používá.
- Dočasné vytváření symbolů
- V některých programovacích jazycích je možné, aby byl nový název proměnné nebo symbol vygenerován a vázán na dočasné umístění. Systém zpracování jazyků zajišťuje, že toto nikdy nebude v rozporu s jiným jménem nebo umístěním v prostředí provádění. Odpovědnost za výběr použití této funkce v těle definice makra je ponechána na programátorovi. Tato metoda byla použita v systému MacLisp , kde k pojmenování
gensymnového názvu symbolu bylo možné použít pojmenovanou funkci . Podobné funkce (obvykle také pojmenovanégensym) existují v mnoha jazycích podobných Lispu, včetně široce implementovaného standardu Common Lisp a Elisp . - Nezkreslený symbol doby čtení
- Toto je podobné prvnímu řešení v tom, že jeden název je sdílen několika rozšířeními stejného makra. Na rozdíl od neobvyklého názvu je však použit nezasunutý symbol doby čtení (označený
#:notací), u kterého není možné se mimo makro vyskytovat. - Balíčky
- Místo neobvyklého názvu nebo nezasazeného symbolu makro jednoduše používá soukromý symbol z balíčku, ve kterém je makro definováno. V uživatelském kódu se symbol náhodně nevyskytuje. Uživatelský kód by se musel dostat dovnitř balíčku pomocí dvojité dvojtečky (
::), aby si dal například oprávnění používat soukromý symbolcool-macros::secret-sym. V tu chvíli je otázka náhodného nedodržení hygieny diskutabilní. Balíkový systém Lisp tak poskytuje životaschopné a úplné řešení problému makro hygieny, který lze považovat za příklad střetu jmen. - Hygienická transformace
- Procesor zodpovědný za transformaci vzorů vstupního formuláře do výstupního formuláře detekuje kolize symbolů a řeší je dočasnou změnou názvů symbolů. Tento druh zpracování podporují systémy pro tvorbu schémat
let-syntaxadefine-syntaxmaker. Základní strategií je identifikovat vazby v definici makra a nahradit tyto názvy gensymy a identifikovat volné proměnné v definici makra a zajistit, aby byla tato jména vyhledávána v rozsahu definice makra místo v rozsahu, kde bylo makro použitý. - Doslovné objekty
- V některých jazycích nemusí rozšíření makra odpovídat textovému kódu; namísto rozbalení na výraz obsahující symbol
fmůže makro vytvořit rozšíření obsahující skutečný objekt, na který odkazujef. Podobně pokud makro potřebuje použít lokální proměnné nebo objekty definované v balíčku makra, může se rozšířit na vyvolání uzavíracího objektu, jehož uzavírající lexikální prostředí je definicí makra.
Implementace
Makro systémy, které automaticky vynucují hygienu, pocházejí ze schématu. Původní algoritmus ( algoritmus KFFD) pro hygienický makro systém představil Kohlbecker v roce '86. V té době nebyl implementací schématu přijat žádný standardní makro systém. Krátce poté, v roce '87, Kohlbecker a Wand navrhli deklarativní vzorový jazyk pro psaní maker, který byl předchůdcem nástroje pro syntax-rulesmakra přijatého standardem R5RS. Syntaktické uzávěry, alternativní hygienický mechanismus, byly navrženy jako alternativa k systému Kohlbecker et al. Od Bawdena a Reese v roce '88. Na rozdíl od algoritmu KFFD syntaktické uzávěry vyžadují, aby programátor výslovně určil rozlišení rozsahu identifikátoru. V roce 1993 Dybvig a kol. představil syntax-casemakro systém, který využívá alternativní reprezentaci syntaxe a udržuje hygienu automaticky. syntax-caseSystém může vyjádřit syntax-rulesjazykem vzor jako odvozené makro.
Termín makro systém může být nejednoznačný, protože v kontextu Scheme může odkazovat jak na konstrukci odpovídající vzoru (např. Pravidla syntaxe), tak na rámec pro reprezentaci a manipulaci se syntaxí (např. Případ syntaxe, syntaktické uzávěry) . Syntax-rules je zařízení pro porovnávání vzorů na vysoké úrovni, které se pokouší usnadnit zápis maker. Není syntax-rulesvšak schopen stručně popsat určité třídy maker a nestačí k vyjádření jiných systémů makra. Syntaxe-pravidla byla popsána v dokumentu R4RS v příloze, ale nebyla nařízena. Později jej R5RS přijal jako standardní makro zařízení. Zde je příklad syntax-rulesmakra, které prohodí hodnotu dvou proměnných:
(define-syntax swap!
(syntax-rules ()
((_ a b)
(let ((temp a))
(set! a b)
(set! b temp)))))
Kvůli nedostatkům čistě syntax-ruleszaloženého makrosystému byly pro Scheme také navrženy a implementovány makro systémy nízké úrovně. Syntax-case je jedním z takových systémů. Na rozdíl od syntax-rulestoho syntax-caseobsahuje jak jazyk pro porovnávání vzorů, tak i zařízení na nízké úrovni pro psaní maker. První umožňuje deklarativní zápis maker, zatímco druhý umožňuje implementaci alternativních frontendů pro psaní maker. Příklad swapu z dřívější doby je téměř identický, syntax-caseprotože jazyk odpovídající vzoru je podobný:
(define-syntax swap!
(lambda (stx)
(syntax-case stx ()
((_ a b)
(syntax
(let ((temp a))
(set! a b)
(set! b temp)))))))
Je však syntax-casevýkonnější než pravidla syntaxe. Například syntax-casemakra mohou určit vedlejší podmínky na svých pravidlech shody vzorů pomocí libovolných funkcí schématu. Alternativně se zapisovatel maker může rozhodnout nepoužívat frontend pro přizpůsobování vzorů a manipulovat přímo se syntaxí. Pomocí této datum->syntaxfunkce mohou makra syntaxe případu také záměrně zachytávat identifikátory, čímž narušují hygienu. Standard R6RS Scheme převzal makro systém syntaxe a případu.
Syntaktické uzávěry a explicitní přejmenování jsou další dva alternativní makro systémy. Oba systémy jsou na nižší úrovni než pravidla syntaxe a ponechávají vynucování hygieny na zapisovači maker. To se liší od pravidel syntaxe i od případu syntaxe, který ve výchozím nastavení automaticky vynucuje hygienu. Zde jsou uvedeny příklady swapů shora pomocí syntaktického uzavření a explicitní implementace přejmenování:
;; 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))))))
Jazyky s hygienickými makro systémy
- Schéma -pravidla syntaxe, případ syntaxe, syntaktické uzávěry a další.
- Racket - odnož Scheme. Jeho makro systém byl původně založen na syntaxi, ale nyní má více funkcí.
- Nemerle
- Dylan
- Elixír
- Nim
- Rez
- Haxe
- Mary2 -makroskopická těla s rozsahem v odvozeném jazyce Algol68 kolem roku 1978
- Julie
- Raku - podporuje jak hygienická, tak nehygienická makra
Kritika
Hygienická makra nabízejí určitou bezpečnost pro programátora na úkor omezení výkonu maker. V přímém důsledku jsou makra Common Lisp mnohem výkonnější než makra Scheme, pokud jde o to, čeho s nimi lze dosáhnout. Doug Hoyte, autor knihy Let Over Lambda , uvedl:
Téměř všechny přístupy ke snížení dopadu zachycování proměnných slouží pouze ke snížení toho, co můžete s defmacro dělat. Hygienická makra jsou v nejlepších situacích bezpečnostní zábradlí pro začátečníky; v nejhorších situacích vytvoří elektrický plot a uvězní své oběti v dezinfikovaném vězení, které je bezpečné pro zajetí.
- Doug Hoyte