Macros

Table of Contents

Bibliografía

El tema está basado en los siguientes materiales. Os recomendamos que los estudieis y, si os interesa y os queda tiempo, que exploréis también en los enlaces que hemos dejado en los apuntes para ampliar información.

Conceptos generales

Muchos lenguajes de programación (sobre todos los más antiguos) incorporan el concepto de macro.

Antes del desarrollo de los lenguajes de programación de alto nivel y de las técnicas de compilación, los programadores de ensamblador debían escribir bloques de código repetitivos en los que cambiaban unos pocos elementos. Para facilitar esta tarea, muchos ensambladores proporcionaban herramientas sofisticadas para tratar estos bloques utilizando expansión de macros. En lugar del código repetitivo, el programador escribía la macro. Después el ensamblador se encargaba de realizar la expansión y sustituir la macro por el código.

De forma muy genérica, una macro consiste en una plantilla o meta-expresión que define un patrón de sustitución. El patrón define unas variables libres y unas expresiones textuales. Cuando usamos la macro, le damos un valor a las variables libres. La macro se expande, aplicando el patrón a los variables y generando código en el lenguaje de programación de la macro.

Una característica fundamental de la macro es que se trata de una técnica de generación de código. En el caso en que el lenguaje sea compilado (como C o C++) la expansión de la macro se realiza en la fase de preprocesamiento del programa, antes de su compilación. El preprocesador expande todas las macros utilizadas y el compilador compila el programa con el código ya expandido. En el caso en que el lenguaje sea interpretado (como LISP o Scheme), la expansión de la macro se realiza al antes de llamar al evaluador. Primero se expande la macro y después se evalua la expresión resultante.

Expansión de macros en C

Por ver un ejemplo concreto, vamos a repasar el sistema de macros en C. Este lenguaje de programación se creó en los años 70 y era natural en aquella época introducir esta característica en el preprocesador del lenguaje. Algunos ejemplos de macros en C:

#define LINE_LEN 80
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define SWAP(a,b) {int t = (a); (a) = (b); (b) = t;}

Como vemos, estas macros contienen expresiones literales y expresiones variables. Las expresiones literales se sustituirán tal cual en la expansión mientras que las expresiones variables se sustituirán por su valor en la llamada a la macro.

Por ejemplo LINE_LEN es una expresión textual que se reemplazará por 80 al expandir la macro. Las dos macros siguientes utilizan las variables libres a y b.

Una llamada a la macro:

x = MAX(p+q, r+s);

se sustituirá por

x = ((p+q) > (r+s) ? (p+q) : (r+s))

El preprocesador analiza textualmente la llamada y aplica la regla definida por la macro, generando el texto resultante. Este texto debe ser una expresión correcta del lenguaje de programación en el que se expande la macro.

Veremos que mediante las macros podemos extender el lenguaje de programación, saliéndonos de su sintaxis y creando un lenguaje propio. Un ejemplo curioso es el siguiente. Supongamos que nos gusta un lenguaje del estilo de Pascal, en lugar de C. Podemos definir las siguientes macros

#define then
#define begin {
#define end ;}

y a continuación:

if (i > 0) then
   begin
      a = 1;
      b = 2
   end

¿Por qué utilizar macros?

Las macros no siguen la sintaxis del lenguaje de programación en el que se programan, sino que son metaprogramas que generan código. Tienen su propia sintaxis de definición y de expansión. Por ello es posible utilizarlos para extender un lenguaje de programación, añadiéndole nuevas características.

Incluso es posible utilizar las macros de un lenguaje de programación minimalista como Scheme para construir prototipos de nuevos lenguajes de programación. Son muy usadas en universidades y departamentos de investigación para crear nuevas primitivas y nuevas lenguajes orientados a dominios específicos. Un ejemplo es la robótica. Se han definido lenguajes de programación específicos orientado al control de tareas y de acciones de robots autónomos utilizando macros definidas en Scheme.

Las macros han sido precursoras de algunas técnicas de meta-programación de los lenguajes modernos. Entendemos por meta-programación la utilización de elementos que se salen del propio lenguaje para extenderlo.

Tres ejemplos concretos son las anotaciones en Java, el Scaffolding en Ruby & Rails y las herramientas de MDA de generación automática de código.

Problemas de la expansión en C

El preprocesador de C extiende la macro de forma ciega, sin realizar ningún tipo de análisis y sin tener ninguna relación con el proceso de compilación posterior. Esto puede producir errores. Por ejemplo, si llamamos a MAX(x++, y++) la expansión de la macro produce el siguiente resultado:

((a++) > (b++) ? (a++) : (b++))

La variable resultante se incrementa dos veces.

Unos pocos lenguajes (sobre todo LISP y Scheme) proporcionan un enfoque alternativo e intengran las macros en el lenguaje de una forma segura y consistentes. Utilizan las llamadas macros higiénicas que encapsulan implícitamente sus argumentos y evitan los errores de las macros de C.

Quasiquotation

Antes de empezar con las macros en Scheme, vamos a introducir la foma especial quasiquote que tiene cierta relación con las macros, en el sentido de que permite también realizar una evaluación selectiva de argumentos.

La forma especial quasiquote se parece a quote porque permite escribir expresiones que se devuelven sin evaluar, pero quasiquote es mucho más potente, porque permite realizar una evaluación de los argumentos que nos interese, precediéndolos con la forma especial unquote.

Por ejemplo, si queremos crear una lista de tres elementos cuyo primer y último elemento sean los literales foo y baz, pero el segundo elemento sea el valor de la variable bar:

(define bar 2)
(quasiquote (foo (unquote bar) baz))  --> (foo 2 baz)

Para hacer más fáciles este tipo de expresiones, Scheme proporciona azúcar sintáctico: el símbolo backquote "`" reemplaza a quasiquote y el carácter coma "," reemplaza a unquote:

`(foo ,bar baz) --> (foo 2 baz)

Más ejemplos:

(define a 2)
(define b 'hola)

'(1 a b)
(quasiquote (1 ,a ,b))
`(1 ,a ,b)
`(1 ,a ,b ,c)
`(1 ,+ ,-)

Macros en Scheme

Como hemos comentado, LISP y sus lenguajes derivados, definen una técnica bastante avanzada de procesamiento de macros. Veremos que una de sus características más importantes es que permiten utilizar argumentos sin evaluar y realizar la evaluación de los mismos cuando nos interese.

Scheme también es muy avanzado en cuanto a la técnica de definición de la macro mediante plantillas y patrones de texto.

Procesamiento de patrones en cadenas de texto

El uso de patrones para el procesamiento de cadenas de texto en lenguajes de programación se remonta al año 1977 con la creación en los laboratorios Bell y AT&T (los mismos en los que se originaron el C y el UNIX) del lenguaje de programación awk. Posteriormente el uso de patrones se incorporó a distintos lenguajes de shell del UNIX y, sobre todo, a Perl, un importante lenguaje de script creado en 1987 por Larry Wall. Una buena introducción a Perl se encuentra en http://perldoc.perl.org/perlintro.html. En http://perldoc.perl.org/perlrequick.html se puede encontrar una buena introducción al uso de expresiones regulares.

Definición de macros en Scheme

Para definir una macro se han de especificar sus reglas de sintaxis (syntax rules). Cada regla consiste en un patrón que muestra una posible estructura para la expresión y una plantilla (template) que determina cómo los componentes de esa estructura se pueden reorganizar para formar una expresión aceptada por Scheme. Cuando se define una macro, el procesador de Scheme memoriza esas reglas de sintaxis. Y cuando encuentra una expresión que encaja con uno de los patrones, automáticamente reorganiza los componentes como dicta el correspondiente template, y evalúa el resultado.

Aunque hablamos de macros, el nombre estándar que reciben las macros en Scheme son extensiones sintácticas. Una extensión sintáctica permite añadir una nueva forma especial al lenguaje de programación. Se definen mediante las construcciones define-syntax y syntax-rules.

Las construcciones define-syntax y syntax-rules se usan con la siguiente sintaxis:

(define-syntax <keyword>
   (syntax-rules (<literales>)
      ((<patron-1> <plantilla-1>)
       ...
       (<patron-n> <plantilla-n>))))

Con define-syntax declaramos el identificador asociado a la macro y con syntax-rules las reglas de expansión de la macro. Estas reglas se definen mediante un conjunto de patrones de texto y de plantillas asociadas. Los literales que se definen después de syntax-rules son identificadores que deben aparecer tal cual en la llamada a la macro y que se usan en los patrones. Los identificadores que aparecen en los patrones y que no se declaran como literales son variables libres que emparejan con las expresiones correspondientes en la llamada a la macro. Las plantillas definen la expansión de la macro, una vez que la llamada a la misma ha emparejado con algún patrón. En las plantillas se usan literales y variables libres. Las variables libres toman el valor resultante del emparejamiento de la llamada a la macro con el patrón.

Expansión de la macro

Los patrones y las plantillas definen unas reglas de transformación de expresiones de texto, al estilo de lenguajes como Perl o awk. En la expansión de la macro se aplican estas reglas y se transforma la llamada a la macro en una expresión resultante de su aplicación. Una vez realizada la transformación, Scheme evalua la expresión resultante.

Veamos un ejemplo en el que todo esto quedará más claro.

En este primer ejemplo definimos la macro mi-or que implementa una o lógica de todas las expresiones que le siguen. Al igual que la or de Scheme, queremos que devuelva #t en cuanto encuentre una expresión cierta, sin seguir evaluando el resto de expresiones. Esta característica hace imposible implementarla como una función.

(define-syntax mi-or
  (syntax-rules ()
    ((mi-or) #t)
    ((mi-or e) e)
    ((mi-or e1 e2 e3 ...)
     (if e1 #t (mi-or e2 e3 ...)))))

En esta macro no definimos literales. Los patrones que definimos son:

  • Patrón 1: (mi-or)
  • Patrón 2: (mi-or e)
  • Patrón 3: (mi-or e1 e2 e3 ...)

Y las plantillas asociadas a esos patrones son:

  • Plantilla 1: #t
  • Plantilla 2: e
  • Plantilla 3: (if e1 #t (mi-or e2 e3 ...))

Los patrones definen posibles formas de llamar a la macro mi-or. En concreto, estamos definiendo tres. En el primer patrón, se define una llamada a mi-or sin argumentos, en el segundo una llamada con un argumento (la expresión e) y en el tercero una llamada con dos o más argumentos (las expresiones e1 e2 e3 ...). Las variables e, e1, e2, e3 son variables libres que emparejarán con alguna expresión de la llamada a la macro. Por último, los puntos suspensivos (...) indican una repetición de 0 o más veces del patrón anterior (en este caso, e3).

Las plantillas definen la expansión de la macro. La primera plantilla indica que hay que sustituir la llamada a la macro por #t. La segunda plantilla indica que hay que escribir la expresión e que se usa como argumento de la llamada a mi-or. Por último, la última plantilla es la que se usa cuando se hace una llamada a mi-or con dos o más argumentos e indica que hay que sustituir esta llamada por (if e1 #t (mi-or e2 e3 ...)), siendo e1, e2, e3 los dos primeros argumentos y el resto de la llamada a la macro.

Por ejemplo, si llamamos a la macro de la siguiente forma:

(mi-or (equal? x 2) #f #t (equal? y 3))

Esta expresión emparejará con el patrón 3, definiendo los siguientes emparejamientos:

Expresión: (mi-or (equal? x 2) #f #t (equal? y 3))
Patron:    (mi-or e1 e2 e3 ...)
Emparejamientos:   e1 <-> (equal? x 2)
                   e2 <-> #f
                   e3 ... <-> #t (equal? y 3)

Al expandir la macro con la plantilla (if e1 #t (mi-or e2 e3 ...)) queda la siguiente expresión:

(if (equal? x 2) #t (mi-or #f #t (equal? y 3)))

Para terminar este primer ejemplo, es interesante comentar que en muchos textos de Scheme la definición de la macro anterior aparecería como sigue:

(define-syntax mi-or
  (syntax-rules ()
    ((_) #t)
    ((_ e) e)
    ((_ e1 e2 e3 ...)
     (if e1 #t (mi-or e2 e3 ...)))))

El símbolo _ es una variable que se empareja con el símbolo que hay al comienzo de la expresión, que siempre será el símbolo mi-or. De hecho, el código anterior sería también equivalente al siguiente, en donde op hace el mismo papel de _.

(define-syntax mi-or
  (syntax-rules ()
    ((op) #t)
    ((op e) e)
    ((op e1 e2 e3 ...)
     (if e1 #t (mi-or e2 e3 ...)))))

Semántica de la llamada a una macro

Veamos ahora unas reglas que sirven de resumen de la semántica de la llamada a una macro.

Para evaluar una llamada a una macro (op exp_1 ... exp_n) debemos seguir las siguientes reglas:

  1. Buscar la definición de la macro. Buscar la forma especial define-syntax en la que aparece op como clave.
  2. Emparejar. Buscar en la definición de la macro la regla sintáctica con la que es posible emparejar la expresión (op exp_1 ... exp_n) que estamos evaluando. Si hay más de una regla con la que se puede emparejar la expresión, escogemos la primera de ellas.
  3. Transformar. Aplicar la regla para transformar la expresión.
  4. Evaluar. Evaluar la expresión resultante. En el caso en que la expresión resultante contenga una llamada a una macro se evaluará siguiendo estas mismas reglas.

Depuración de las macros

Para depurar las macros, muchas veces es conveniente comprobar el resultado de su expansión, sin dejar que se evaluen. Una forma de hacerlo es añadir un quote al comienzo de las plantillas. El quote hará que la expresión resultante no se evalue, sino que se devuelva como una lista o un símbolo.

Por ejemplo, si hacemos esto con la primera macro que hemos visto en este tema tendremos la siguiente macro.

(define-syntax mi-or
  (syntax-rules ()
    ((mi-or) '#t)
    ((mi-or e) 'e)
    ((mi-or e1 e2 e3 ...)
     '(if e1 #t (mi-or e2 e3 ...)))))

Las llamadas a esta macro siempre devolverán expresiones sin evaluar resultantes de la expansión de la macro:

>(mi-or (equal? a 1))
(equal? a 1)
>(mi-or (equal? a 1)
         (equal? a 2)
         #t)
(if (equal? a 1) #t (mi-or (equal? a 2) #t))

Ejemplos de macros

Veamos algunos ejemplos de macros en Scheme.

make-procedure

La macro make-procedure es una forma de hacer más legible la forma especial lambda. Tiene la misma sintaxis que lambda y se transforma en una llamada a esa forma especial.

(define-syntax make-procedure
    (syntax-rules ()
      ((make-procedure (x ...) expr ...)
       (lambda (x ...) expr ...))))

Por ejemplo

(make-procedure (x) (* x x)) 

se transforma en

(lambda (x) (* x x))

mi-let

La macro mi-let explica cómo se implementa el let en Scheme, transformándose en una llamada a lambda para construir una función que tiene a las variables del let como argumentos y una posterior llamada a esta función con los valores como parámetros.

(define-syntax mi-let
  (syntax-rules ()
    ((mi-let ((x v) ...) e ...)
     ((lambda (x ...) e ...) v ...))))

Por ejemplo:

(mi-let ((x 1) 
      (y (+ 2 1)) 
      (z (lambda (x y) (+ x y))))
(z x y))

se transforma en

((lambda (x y z) 
    (z x y)) 1 (+ 2 1) (lambda (x y) (+ x y)))

mi-cond

En el siguiente ejemplo se define la macro mi-cond que se comporta igual que la forma especial cond de Scheme, aunque variando ligeramente la sintaxis. En la macro definimos los identificadores -> y else como literales. Deben aparecer como tales en la invocación a la macro.

(define-syntax mi-cond
   (syntax-rules (-> else)
     ((mi-cond (else -> expr))
      expr)
     ((mi-cond (test1 -> expr1))
      (if test1 expr1))
     ((mi-cond (test1 -> expr1) (test2 -> expr2) ...)
      (if test1 expr1 (mi-cond (test2 -> expr2) ...)))))

El siguiente ejemplo muestra cómo se realiza una llamada a la macro. Podemos comprobar cómo se utilizan los literales -> y else para definir nuestra propia sintaxis de mi-cond.

(mi-cond ((equal? a 1) -> 1)
         ((equal? a 2) -> 2)
         (else -> 3))

Macros paradigma procedural

Los siguientes ejemplos contienen la forma especial begin que hace que se evaluen de forma secuencial un conjunto de expresiones de Scheme.

(define-syntax multi-print
  (syntax-rules ()
    ((multi-print arg1 arg2 ...) 
     (begin (print arg1)
            (newline)
            (multi-print arg2 ...)))
    ((multi-print) #t)))
(define-syntax when
    (syntax-rules ()
      ((when condition expr1 expr2 ...)
       (if condition (begin expr1 expr2 ...) #f))))




Lenguajes y Paradigmas de Programación
Curso 2010-2011
Departamento de Ciencia de la Computación e Inteligencia Artificial
Universidad de Alicante

Sitio web realizado con org-mode y el estilo CSS del proyecto Worg

Validate XHTML 1.0