En informática , las macros higiénicas son macros cuya expansión está garantizada para no causar la captura accidental de identificadores . Son una característica de lenguajes de programación como Scheme , [1] Dylan , [2] Rust , Nim y Julia . El problema general de la captura accidental era bien conocido en la comunidad Lisp antes de la introducción de las macros higiénicas. Los escritores de macros usarían características del lenguaje que generarían identificadores únicos (por ejemplo, gensym) o usarían identificadores ofuscados para evitar el problema. Las macros higiénicas son una solución programática al problema de captura que está integrada en el expansor de macros. El término "higiene" fue acuñado en el artículo de Kohlbecker et al. de 1986 que introdujo la expansión de macros higiénicas, inspirada en la terminología utilizada en matemáticas. [3]
En los lenguajes de programación que tienen sistemas de macros no higiénicos, es posible que las vinculaciones de variables existentes se oculten de una macro mediante vinculaciones de variables que se crean durante su expansión. En C , este problema se puede ilustrar con el siguiente fragmento:
#define INCI(i) { int a=0; ++i; } int main ( void ) { int a = 4 , b = 8 ; INCI ( a ); INCI ( b ); printf ( "a ahora es %d, b ahora es %d \n " , a , b ); return 0 ; }
Ejecutar lo anterior a través del preprocesador C produce:
int main ( void ) { int a = 4 , b = 8 ; { int a = 0 ; ++ a ; }; { int a = 0 ; ++ b ; }; printf ( "a ahora es %d, b ahora es %d \n " , a , b ); devuelve 0 ; }
La variable a
declarada en el ámbito superior queda oculta por la a
variable en la macro, lo que introduce un nuevo ámbito . Como resultado, a
nunca se ve alterada por la ejecución del programa, como lo muestra la salida del programa compilado:
a ahora es 4, b ahora es 9
El problema de la higiene puede extenderse más allá de las vinculaciones de variables. Considere esta macro de Common Lisp :
( defmacro my-unless ( condición &body cuerpo ) ` ( if ( no , condición ) ( progn ,@ cuerpo )))
Si bien no hay referencias a variables en esta macro, se supone que los símbolos "if", "not" y "progn" están todos vinculados a sus definiciones habituales en la biblioteca estándar. Sin embargo, la macro anterior se utiliza en el siguiente código:
( flet (( not ( x ) x )) ( my-unless t ( format t "¡Esto no debería imprimirse!" )))
La definición de "no" ha sido alterada localmente y con ello la expansión de my-unless
los cambios.
Sin embargo, tenga en cuenta que para Common Lisp este comportamiento está prohibido, según 11.1.2.1.2 Restricciones del paquete COMMON-LISP para programas conformes. De todos modos, también es posible redefinir funciones por completo. Algunas implementaciones de Common Lisp proporcionan bloqueos de paquetes para evitar que el usuario cambie las definiciones de los paquetes por error.
Por supuesto, el problema puede ocurrir de manera similar para funciones definidas por el programa:
( defun operador definido por el usuario ( cond ) ( no cond )) ( defmacro my-unless ( condición &body cuerpo ) ` ( if ( operador definido por el usuario , condición ) ( progn ,@ cuerpo ))) ; ... más tarde ...( flet (( operador-definido-por-el-usuario ( x ) x )) ( my-unless t ( format t "¡Esto no debería imprimirse!" )))
El sitio de uso redefine user-defined-operator
y, por lo tanto, cambia el comportamiento de la macro.
El problema de la higiene se puede resolver con macros convencionales utilizando varias soluciones alternativas.
La solución más simple, si se necesita almacenamiento temporal durante la expansión de una macro, es utilizar nombres de variables inusuales en la macro con la esperanza de que el resto del programa nunca utilice los mismos nombres.
#define INCI(i) { int INCIa = 0; ++i; } int main ( void ) { int a = 4 , b = 8 ; INCI ( a ); INCI ( b ); printf ( "a ahora es %d, b ahora es %d \n " , a , b ); return 0 ; }
INCIa
Hasta que se cree una variable denominada , esta solución produce el resultado correcto:
a ahora es 5, b ahora es 9
El problema está resuelto para el programa actual, pero esta solución no es robusta. El programador debe mantener sincronizadas las variables utilizadas dentro de la macro y las del resto del programa. En concreto, el uso de la macro INCI
en una variable INCIa
va a fallar de la misma forma que la macro original falló en una variable a
.
En algunos lenguajes de programación, es posible generar un nuevo nombre de variable o símbolo y vincularlo a una ubicación temporal. El sistema de procesamiento del lenguaje garantiza que esto nunca entre en conflicto con otro nombre o ubicación en el entorno de ejecución. La responsabilidad de elegir usar esta característica dentro del cuerpo de una definición de macro se deja al programador. Este método se utilizó en MacLisp , donde una función nombrada gensym
podía usarse para generar un nuevo nombre de símbolo. gensym
Existen funciones similares (generalmente también nombradas) en muchos lenguajes similares a Lisp, incluido el estándar Common Lisp ampliamente implementado [4] y Elisp .
Aunque la creación de símbolos resuelve el problema del sombreado de variables, no resuelve directamente el problema de la redefinición de funciones. [5] Sin embargo, gensym
las macros, las funciones de biblioteca estándar y las macros higiénicas son suficientes para incorporar macros higiénicas en un lenguaje no higiénico. [6]
Esto es similar a la ofuscación en el sentido de que un nombre único es compartido por múltiples expansiones de la misma macro. Sin embargo, a diferencia de un nombre inusual, se utiliza un símbolo no interno en el momento de la lectura (indicado por la #:
notación), para el cual es imposible que aparezca fuera de la macro, de manera similar a gensym
.
Al utilizar paquetes como Common Lisp, la macro simplemente utiliza un símbolo privado del paquete en el que se define la macro. El símbolo no aparecerá accidentalmente en el código de usuario. El código de usuario tendría que acceder al interior del paquete utilizando la ::
notación de dos puntos ( ) para darse permiso para utilizar el símbolo privado, por ejemplo cool-macros::secret-sym
. En ese punto, el problema de la falta accidental de higiene es discutible. Además, el estándar ANSI Common Lisp categoriza la redefinición de funciones y operadores estándar, global o localmente, como invocación de un comportamiento indefinido . Por lo tanto, la implementación puede diagnosticar dicho uso como erróneo. Por lo tanto, el sistema de paquetes Lisp proporciona una solución viable y completa al problema de la higiene de macros, que puede considerarse como un ejemplo de conflicto de nombres.
Por ejemplo, en el ejemplo de redefinición de una función definida por el programa, la my-unless
macro puede residir en su propio paquete, donde user-defined-operator
es un símbolo privado en ese paquete. El símbolo user-defined-operator
que aparece en el código de usuario será entonces un símbolo diferente, no relacionado con el utilizado en la definición de la my-unless
macro.
En algunos lenguajes, la expansión de una macro no necesita corresponderse con un código textual; en lugar de expandirse a una expresión que contenga el símbolo f
, una macro puede producir una expansión que contenga el objeto real al que hace referencia f
. De manera similar, si la macro necesita utilizar variables locales u objetos definidos en el paquete de la macro, puede expandirse a una invocación de un objeto de cierre cuyo entorno léxico envolvente sea el de la definición de la macro.
Los macrosistemas higiénicos en lenguajes como Scheme utilizan un proceso de expansión de macros que preserva el alcance léxico de todos los identificadores y evita la captura accidental. Esta propiedad se denomina transparencia referencial . En los casos en los que se desea la captura, algunos sistemas permiten al programador violar explícitamente los mecanismos de higiene del macrosistema.
let-syntax
Por ejemplo, los sistemas de creación de macros y de Scheme define-syntax
son higiénicos, por lo que la siguiente implementación de Scheme my-unless
tendrá el comportamiento deseado:
( define-sintaxis mi-a-menos ( reglas-de-sintaxis () (( _ cuerpo de la condición ... ) ( if ( no condición ) ( comienzo cuerpo ... ))))) ( let (( not ( lambda ( x ) x ))) ( my-unless #t ( display "¡Esto no debería imprimirse!" ) ( nueva línea )))
El macroprocesador higiénico responsable de transformar los patrones del formato de entrada en un formato de salida detecta conflictos entre símbolos y los resuelve modificando temporalmente los nombres de los símbolos. La estrategia básica es identificar enlaces en la definición del macro y reemplazar esos nombres con gensyms, e identificar variables libres en la definición del macro y asegurarse de que esos nombres se busquen en el ámbito de la definición del macro en lugar del ámbito en el que se utilizó el macro.
Los macrosistemas que hacen cumplir automáticamente la higiene se originaron con Scheme. El algoritmo KFFD original para un macrosistema higiénico fue presentado por Kohlbecker en 1986. [3] En ese momento, las implementaciones de Scheme no adoptaron ningún macrosistema estándar. Poco después, en 1987, Kohlbecker y Wand propusieron un lenguaje declarativo basado en patrones para escribir macros, que fue el predecesor de la syntax-rules
función de macro adoptada por el estándar R5RS. [1] [7] Bawden y Rees propusieron en 1988 cierres sintácticos, un mecanismo de higiene alternativo, como alternativa al sistema de Kohlbecker et al. [8] A diferencia del algoritmo KFFD, los cierres sintácticos requieren que el programador especifique explícitamente la resolución del alcance de un identificador. En 1993, Dybvig et al. introdujeron el syntax-case
macrosistema, que utiliza una representación alternativa de la sintaxis y mantiene la higiene automáticamente. [9] El syntax-case
sistema puede expresar el syntax-rules
lenguaje de patrones como un macroderivado. El término macrosistema puede ser ambiguo porque, en el contexto de Scheme, puede referirse tanto a una construcción de coincidencia de patrones (por ejemplo, reglas de sintaxis) como a un marco para representar y manipular la sintaxis (por ejemplo, sintaxis-caso, cierres sintácticos).
Syntax-rules es una herramienta de comparación de patrones de alto nivel que intenta hacer que las macros sean más fáciles de escribir. Sin embargo, syntax-rules
no puede describir de forma sucinta ciertas clases de macros y es insuficiente para expresar otros sistemas de macros. Syntax-rules se describió en el documento R4RS en un apéndice, pero no se impuso. Más tarde, R5RS lo adoptó como una herramienta de macros estándar. A continuación, se muestra un ejemplo de syntax-rules
macro que intercambia el valor de dos variables:
( define-sintaxis swap! ( reglas-sintaxis () (( _ a b ) ( let (( temp a )) ( set! a b ) ( set! b temp )))))
Debido a las deficiencias de un syntax-rules
sistema de macros puramente basado en mayúsculas y minúsculas, el estándar R6RS Scheme adoptó el sistema de macros de sintaxis-caso. [10] A diferencia de syntax-rules
, syntax-case
contiene tanto un lenguaje de coincidencia de patrones como una función de bajo nivel para escribir macros. El primero permite escribir macros de forma declarativa, mientras que el segundo permite la implementación de interfaces alternativas para escribir macros. El ejemplo de intercambio anterior es casi idéntico en syntax-case
porque el lenguaje de coincidencia de patrones es similar:
( define-sintaxis swap! ( lambda ( stx ) ( sintaxis-case stx () (( _ a b ) ( sintaxis ( let (( temp a )) ( set! a b ) ( set! b temp )))))))
Sin embargo, syntax-case
es más potente que las reglas de sintaxis. Por ejemplo, syntax-case
las macros pueden especificar condiciones secundarias en sus reglas de coincidencia de patrones a través de funciones de Scheme arbitrarias. Alternativamente, un escritor de macros puede elegir no usar la interfaz de coincidencia de patrones y manipular la sintaxis directamente. Al usar la datum->syntax
función, las macros de sintaxis-case también pueden capturar identificadores intencionalmente, rompiendo así la higiene.
También se han propuesto e implementado otros sistemas de macros para Scheme. Los cierres sintácticos y el cambio de nombre explícito [11] son dos sistemas de macros alternativos. Ambos sistemas son de nivel inferior a las reglas de sintaxis y dejan la aplicación de la higiene al escritor de la macro. Esto difiere tanto de las reglas de sintaxis como de la sintaxis de mayúsculas y minúsculas, que aplican automáticamente la higiene de forma predeterminada. Los ejemplos de intercambio anteriores se muestran aquí utilizando una implementación de cierre sintáctico y cambio de nombre explícito respectivamente:
;; cierres sintácticos ( define-sintaxis swap! ( sc-macro-transformer ( lambda ( form environment ) ( let (( a ( close-sintaxis ( cadr form ) environment )) ( b ( close-sintaxis ( caddr form ) environment ))) ` ( let (( temp , a )) ( set! , a , b ) ( set! , b temp )))))) ;; cambio de nombre explícito ( define-sintaxis swap! ( er-macro-transformer ( lambda ( form renombrar comparar ) ( let (( a ( cadr form )) ( b ( caddr form )) ( temp ( renombrar 'temp ))) ` ( , ( renombrar 'let ) (( , temp , a )) ( , ( renombrar 'set! ) , a , b ) ( , ( renombrar 'set! ) , b , temp ))))))
Las macros higiénicas ofrecen seguridad y transparencia referencial a costa de hacer que la captura intencional de variables sea menos sencilla. Doug Hoyte, autor de Let Over Lambda , escribe: [16]
Casi todos los enfoques adoptados para reducir el impacto de la captura variable sólo sirven para reducir lo que se puede hacer con defmacro. Los macros higiénicos son, en el mejor de los casos, una barandilla de seguridad para principiantes; en el peor de los casos, forman una valla eléctrica que atrapa a sus víctimas en una prisión higienizada y segura para la captura.
—Doug Hoyte
Muchos sistemas macro higiénicos ofrecen salidas de emergencia sin comprometer las garantías que brinda la higiene; por ejemplo, Racket permite definir parámetros de sintaxis que permiten introducir variables ligadas de forma selectiva. Gregg Henderschott ofrece un ejemplo en Fear of Macros [17] de implementación de un operador if anafórico de esta manera.