Macro higiénica - Hygienic macro

Las macros higiénicas son macros cuya expansión está garantizada para no provocar la captura accidental de identificadores . Son una característica de los lenguajes de programación como Scheme , Dylan , Rust , Nim y Julia . El problema general de la captura accidental era bien conocido dentro de la comunidad Lisp antes de la introducción de 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 se integra en el propio expansor de macros. El término "higiene" fue acuñado en el artículo de 1986 de Kohlbecker et al. Que introdujo la macroexpansión higiénica, inspirada en la terminología utilizada en matemáticas.

El problema de la higiene

En los lenguajes de programación que tienen sistemas de macros no higiénicos, es posible que los enlaces de variables existentes se oculten de una macro mediante enlaces de variables que se crean durante su expansión. En C , este problema se puede ilustrar con el siguiente fragmento:

#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;
}

Ejecutar lo anterior a través del preprocesador de C produce:

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;
}

La variable adeclarada en el ámbito superior está sombreada por la avariable de la macro, lo que introduce un nuevo ámbito . Como resultado, nunca es alterado por la ejecución del programa, como muestra la salida del programa compilado:

a is now 4, b is now 9

La solución más simple es dar a las variables de macros nombres que no entren en conflicto con ninguna variable en el programa actual:

#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;
}

Hasta que INCIase crea una variable nombrada , esta solución produce la salida correcta:

a is now 5, b is now 9

El problema está resuelto para el programa actual, pero esta solución no es sólida. El programador debe mantener sincronizadas las variables utilizadas dentro de la macro y las del resto del programa. Específicamente, el uso de la macro INCIen una variable INCIafallará de la misma manera que la macro original falló en una variable a.

El "problema de la higiene" puede extenderse más allá de las fijaciones variables. Considere esta macro Common Lisp :

(defmacro my-unless (condition &body body)
 `(if (not ,condition)
    (progn
      ,@body)))

Si bien no hay referencias a variables en esta macro, se asume que los símbolos "si", "no" y "progn" están vinculados a sus definiciones habituales. Sin embargo, si se utiliza la macro anterior en el siguiente código:

(flet ((not (x) x))
  (my-unless t
    (format t "This should not be printed!")))

La definición de "no" se ha modificado localmente y, por lo tanto, la expansión de los my-unlesscambios. (La redefinición de funciones y operadores estándar, global o localmente, en realidad invoca un comportamiento indefinido de acuerdo con ANSI Common Lisp. Tal uso puede ser diagnosticado por la implementación como erróneo).

Por otro lado, los sistemas macro higiénicos conservan el alcance léxico de todos los identificadores (como "si" y "no") automáticamente. Esta propiedad se llama transparencia referencial .

Por supuesto, el problema puede ocurrir para funciones definidas por programa que no están protegidas de la misma manera:

(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!")))

La solución de Common Lisp a este problema es utilizar paquetes. La my-unlessmacro puede residir en su propio paquete, donde user-defined-operatorhay un símbolo privado en ese paquete. El símbolo que user-defined-operatoraparece en el código de usuario será entonces un símbolo diferente, sin relación con el utilizado en la definición de la my-unlessmacro.

Mientras tanto, lenguajes como Scheme que utilizan macros higiénicas evitan la captura accidental y aseguran la transparencia referencial automáticamente como parte del proceso de expansión macro. En los casos en que se desea la captura, algunos sistemas permiten al programador violar explícitamente los mecanismos de higiene del macro sistema.

Por ejemplo, la siguiente implementación de Scheme my-unlesstendrá el comportamiento deseado:

(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)))

Estrategias utilizadas en lenguajes que carecen de macros higiénicas

En algunos lenguajes como Common Lisp , Scheme y otros de la familia de lenguajes Lisp , las macros proporcionan un medio poderoso para extender el lenguaje. Aquí la falta de higiene en las macros convencionales se resuelve mediante varias estrategias.

Ofuscación
Si se necesita almacenamiento temporal durante la expansión de una macro, se pueden usar nombres de variables inusuales con la esperanza de que nunca se usen los mismos nombres en un programa que usa la macro.
Creación de símbolo temporal
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 idioma asegura que esto nunca entre en conflicto con otro nombre o ubicación en el entorno de ejecución. La responsabilidad de elegir utilizar esta función dentro del cuerpo de una definición de macro se deja al programador. Este método se usó en MacLisp , donde una función nombrada gensympodría usarse para generar un nuevo nombre de símbolo. Existen funciones similares (generalmente también nombradas gensym) en muchos lenguajes similares a Lisp, incluido el estándar Common Lisp ampliamente implementado y Elisp .
Símbolo no internado en tiempo de lectura
Esto es similar a la primera solución en que un solo nombre es compartido por múltiples expansiones de la misma macro. Sin embargo, a diferencia de un nombre inusual, se usa un símbolo de tiempo de lectura no internado (indicado por la #:notación), para el cual es imposible que ocurra fuera de la macro.
Paquetes
En lugar de un nombre inusual o un símbolo no internado, la macro simplemente usa 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 llegar al interior del paquete usando la ::notación de dos puntos ( ) para darse permiso para usar el símbolo privado, por ejemplo cool-macros::secret-sym. En ese momento, el problema de la falta de higiene accidental es discutible. Por lo tanto, el sistema de paquetes Lisp proporciona una solución viable y completa al problema de la higiene macro, que puede considerarse como una instancia de conflicto de nombres.
Transformación higiénica
El procesador responsable de transformar los patrones del formulario de entrada en un formulario de salida detecta los conflictos de símbolos y los resuelve cambiando temporalmente los nombres de los símbolos. Este tipo de procesamiento es compatible con los sistemas de creación de macros let-syntaxy de Scheme define-syntax. La estrategia básica es identificar enlaces en la definición de macro y reemplazar esos nombres con gensyms, e identificar variables libres en la definición de macro y asegurarse de que esos nombres se busquen en el alcance de la definición de macro en lugar del alcance donde se encontraba la macro. usó.
Objetos literales
En algunos idiomas, la expansión de una macro no necesita corresponder al 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 usar 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 adjunto es el de la definición de la macro.

Implementaciones

Los sistemas macro que hacen cumplir automáticamente la higiene se originaron con Scheme. El algoritmo original ( algoritmo KFFD) para un sistema macro higiénico fue presentado por Kohlbecker en el '86. En ese momento, las implementaciones de Scheme no adoptaron ningún sistema macro estándar. Poco después, en el 87, Kohlbecker y Wand propusieron un lenguaje basado en patrones declarativos para escribir macros, que fue el predecesor de la syntax-rulesfacilidad de macros adoptada por el estándar R5RS. Los cierres sintácticos, un mecanismo de higiene alternativo, fueron propuestos como una alternativa al sistema de Kohlbecker et al. Por Bawden y Rees en el '88. 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. Introdujo el syntax-casesistema de macros, que utiliza una representación alternativa de sintaxis y mantiene la higiene de forma automática. El syntax-casesistema puede expresar el syntax-ruleslenguaje de patrones como una macro derivada.

El término macro sistema 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, caso de sintaxis, cierres sintácticos) . Syntax-rules es una función de coincidencia de patrones de alto nivel que intenta hacer que las macros sean más fáciles de escribir. Sin embargo, syntax-rulesno es capaz de describir sucintamente ciertas clases de macros y es insuficiente para expresar otros macro sistemas. Las reglas de sintaxis se describieron en el documento R4RS en un apéndice, pero no fueron obligatorias. Más tarde, R5RS lo adoptó como una instalación macro estándar. Aquí hay una syntax-rulesmacro de ejemplo que intercambia el valor de dos variables:

(define-syntax swap!
  (syntax-rules ()
    ((_ a b)
     (let ((temp a))
       (set! a b)
       (set! b temp)))))

Debido a las deficiencias de un syntax-rulesmacro sistema puramente basado, también se han propuesto e implementado macro sistemas de bajo nivel para el Esquema. Syntax-case es uno de esos sistemas. A diferencia syntax-rules, syntax-casecontiene un lenguaje de coincidencia de patrones y 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 de antes es casi idéntico syntax-caseporque el lenguaje de coincidencia de patrones es similar:

(define-syntax swap!
  (lambda (stx)
    (syntax-case stx ()
      ((_ a b)
       (syntax
        (let ((temp a))
          (set! a b)
          (set! b temp)))))))

Sin embargo, syntax-casees más poderoso que las reglas de sintaxis. Por ejemplo, las syntax-casemacros pueden especificar condiciones laterales en sus reglas de coincidencia de patrones mediante funciones de esquema arbitrarias. Alternativamente, un escritor de macros puede optar por no utilizar la interfaz de coincidencia de patrones y manipular la sintaxis directamente. Usando la datum->syntaxfunción, las macros de mayúsculas y minúsculas también pueden capturar identificadores intencionalmente, rompiendo así la higiene. El estándar R6RS Scheme adoptó el sistema de macros de sintaxis-case.

Los cierres sintácticos y el cambio de nombre explícito son otros dos sistemas macro alternativos. Ambos sistemas son de un nivel más bajo que las reglas de sintaxis y dejan la aplicación de la higiene al escritor de macros. Esto difiere tanto de las reglas de sintaxis como de las mayúsculas y minúsculas, que imponen automáticamente la higiene de forma predeterminada. Los ejemplos de intercambio de arriba se muestran aquí usando un cierre sintáctico y una implementación explícita de cambio de nombre respectivamente:

;; 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))))))

Idiomas con macrosistemas higiénicos

  • Esquema : reglas de sintaxis, mayúsculas y minúsculas, cierres sintácticos y otros.
  • Racket : una rama de Scheme. Su sistema de macros se basaba originalmente en mayúsculas y minúsculas, pero ahora tiene más funciones.
  • Nemerle
  • Dylan
  • Elixir
  • Nim
  • Oxido
  • Haxe
  • Mary2 - macrocuerpos con ámbito en un lenguaje derivado de Algol68 alrededor de 1978
  • Julia
  • Raku : admite macros higiénicas y antihigiénicas

Crítica

Las macros higiénicas ofrecen cierta seguridad para el programador a expensas de limitar la potencia de las macros. Como consecuencia directa, las macros Common Lisp son mucho más poderosas que las macros Scheme, en términos de lo que se puede lograr con ellas. Doug Hoyte, autor de Let Over Lambda , declaró:

Casi todos los enfoques adoptados para reducir el impacto de la captura de variables solo sirven para reducir lo que puede hacer con defmacro. Las macros higiénicas son, en el mejor de los casos, una barandilla de seguridad para principiantes; en la peor de las situaciones, forman una cerca eléctrica, atrapando a sus víctimas en una prisión desinfectada y segura.

-  Doug Hoyte

Ver también

Notas

Referencias