jueves, 4 de febrero de 2010

Haciendo un intérprete de LISP (XVII)

El LISP es un lenguaje homoicónico. Es decir, las expresiones son valores en sí. Esto permite trabajar con el código como si fuera dato. Por esa razón he puesto el tag "programación genérica" en todos las entradas del intérprete lisp. En esta entrada vamos a ver qué podemos hacer gracias a esta propiedad.

Carga de ficheros

La carga de ficheros es bien simple. Lo cargamos como dato y, dado que los datos son también código, lo ejecutamos. Existen un gran inconveniente a esta forma de trabajar: Si queremos usar el código varias veces tendríamos que cargarlo y ejecutarlo varias veces. Esto es debido a que podríamos usar diferentes entornos de ejecución cada vez que carguemos el fichero. Al tener los símbolos distintos significados, cada ejecución tendría lugar a diferentes resultados.

Hay varias soluciones a esto.

  1. Admitirlo. Esto es lo que se ha llamado siempre incluir ficheros.
  2. Usar siempre el entorno global cuando se cargan ficheros.
  3. No permitir expresiones que usen nombres del entorno en el fichero a cargar.

En concreto, la tercera solución es muy atractiva. Encerrar el código en algún tipo de construcción de forma que los nombres se extraigan del entorno de uso, no del entorno de carga, nos lleva de forma natural al concepto de macro.

De las funciones a las macros

El gran problema del uso de datos como si fuera código es que perdemos el entorno. Si uso un nombre "x", dependiendo de donde lo use, significará una cosa u otra. Si sé que lo voy a usar inmediatamente está claro que se refiere al entorno actual, pero si voy a atrapar esa "x" en un trozo de dato que voy a usar luego... ¿qué valdrá esa "x" cuando se use? No lo sabemos.

Esta es una de las razones por las que Scheme usa las clausuras sintácticas a la hora de utilizar macros. De esta manera cuando convertimos código en dato también nos llevamos el entorno en los datos. Pero no seguiremos por ese camino. Miremos antes algo más simple. ¿Por qué no ocurren estos problemas con las funciones?

El código que se ejecuta en las funciones lo hace en tres posibles entornos. Esto ya está explicado aquí. Lo interesante de este modelo es que, use donde use cualquier nombre, éste ha de estar definido en el mismo trozo de código un poco más arriba. Tanto en la evaluación de sus argumentos como en la evaluación de la función.

Esto se observa en el siguiente diagrama.


En las funciones los entornos se evalúan donde se escriben, en el sitio de llamada (el entorno de uso). El cuerpo se evalúa donde se escribe también (en el entorno local). El resultado se usa tal cual.

En una macro lo que queremos es evaluar código en el entorno de uso. Tal como hacíamos en la inclusión. Es la parte de la derecha de la figura anterior. Como se observa, se evalúa el código generado en el entorno local fuera, en el entorno de uso. Además, se usan los argumentos sin evaluar. Esto lleva a todos los problemas conocidos de las macros (ver por ejemplo el libro de Paul Graham, On Lisp).

Realmente la evaluación de la macro se hace en un paso especial que se denomina macro expansión. Este paso es especial porque se tiene que ejecutar en tiempo de compilación mientras que la simple evaluación se compila. En nuestro caso, en el que tenemos un intérprete, esta diferencia es únicamente nominal.

Más allá de las macros

Viendo el diagrama, existen otras posibilidades de llamadas compuestas; aparte de las macros y las funciones. Además de las que evalúen algunos argumentos sí y otros no.

La mezcla de macro y función no es usual, pero las FEXPR sí que se ven de vez en cuando.

0 comentarios:

Publicar un comentario