Mostrando entradas con la etiqueta macros. Mostrar todas las entradas
Mostrando entradas con la etiqueta macros. Mostrar todas las entradas

martes, 8 de febrero de 2011

Las tres cosas que diferencian funciones y macros

Hace tiempo hablé sobre las similitudes entre funciones y macros. Esta vez voy a hablar de sus diferencias.

Hay tres cosas que una macro puede hacer y una función no.

1) El control del flujo de ejecución

Una función no puede controlar qué pasa con sus argumentos. No puede controlar si se evalúan o no. No puede recuperarse de un error si uno de sus argumentos falla. No puede hacer nada. De hecho, cuando evaluamos

f(1+2, 3*4)

Lo que ocurre es que primero evaluamos los argumentos.

f(3, 12)

Y luego le pasamos el control a "f" que hará lo que tenga que hacer. Esto no es problema en un lenguaje funcional ya que no vamos a tener efectos secundarios de ningún tipo, pero sí que lo es en un lenguaje con efectos secundarios.

haz_dos_veces(print("a"))

Si "haz_dos_veces" es una función, vamos a evaluar primero los argumentos. Se imprimirá una vez "a" y pasaremos el resultado (posiblemente de tipo unitario) a la función que, haga lo que haga, no va a poder reimprimir la "a".

En el caso de que tuviéramos referencias a funciones entonces todo cambia. Ahora se puede pasar en el argumento la referencia a la función que hace lo que yo quiero.

define_funcion f() { print("a") }
haz_dos_veces(f)

Ahora sí que "haz_dos_veces" puede llamar dos veces a la función pasada e imprimir "a" dos veces. Así que cuando hay referencias a funciones esta diferencia entre macros y funciones no cuenta tanto; pero cuenta porque hay que envolver en una función el código que queremos pasar.

Las macros pueden hacerlo sin envolver nada.

define_macro haz_dos_veces(f) { f; f }
haz_dos_veces(print("a"))

Se expande a

print("a");print("a")


2) Manejo de entornos

El ejemplo más claro de esta posibilidad es la definición de símbolos en el entorno dinámico, aunque no sea lo más seguro y fácil de entender.

define_macro haz_a_cero() { a=0; }
a=1;
haz_a_cero(); //Expande a   a=0
print(a) //Imprime 0 porque ha sido modificado por la macro

Si fuera una función, hacer "a" valer cero dentro de la función sólo modifica el entorno local de la función.

define_funcion haz_a_cero() { a=0; }
a=1;
haz_a_cero();
print(a) //Imprime 1 porque no se ha modificado nada

Como ocurría con el apartado anterior, si tenemos referencias a variables (punteros), una función podría simular este comportamiento.

define_funcion haz_cero(x) {*x=0; }
a=1;
haz_cero(&a);
print(a) //Imprime 0

Sin embargo, la inspección del entorno la hemos tenido que hacer en la llamada con el "&a". La función no puede elegir qué símbolo va a cambiar. Si hubiéramos pasado "&b" la función no se quejaría y cambiaría "b". Con la macro esto no ocurría ya que ella misma inspeccionaba el entorno dinámico.


3) Procesado de código

Esta es la diferencia realmente grande. Los argumentos de una macro pueden significar algo completamente distinto a lo que se cree porque la macro puede procesar el código y cambiarlo. Por ejemplo, la macro "bromista" cambia los "+" por "-".

bromista(5+3) //Expande a  5-3

Con este tipo de transformaciones lo que realmente tenemos es la capacidad de modificar la semántica. En cierta forma, estamos ya acostumbrados. Supongamos un lenguaje en el que tengamos que declarar variables antes de usarlas, como JavaScript.

f(a=3); //Error, a no definido. 
var a=3; //Define a y le da el valor 3

El significado de "=" cuando se evalúa y cuando se escribe tras "var" es distinto. Si contemplamos "var" como una macro, lo que hace es buscar expresiones con la forma identificador-igual-valor. Para cada una de esas expresiones (en vez de evaluarlas como hizo la función "f"), define la variable en el entorno y le da ese valor.

Es esta capacidad de analizar y transformar el código de sus argumentos lo que hacen las macros únicas y superiores a las funciones.

miércoles, 8 de diciembre de 2010

C++: Conceptos y traits

Toda esta entrada es una especie de resumen personal de la presentación de Bartosz Milewski que podéis ver aquí más algunas ideas extra de mi propia cosecha.

Conceptos mediante traits

Antes de nada hay que entender que los template en C++ no pueden compilarse inmediatamente porque no sabemos los tipos que vamos a tener cuando instanciemos el template.

El problema podría ser resuelto si forzamos a que estos tipos cumplan un contrato, una interfaz. La idea es hacerlo mediante los llamados conceptos. Por ejemplo, un tipo sobre el que podamos comprobar la igualdad, tendría un concepto parecido a este:

concept Eq<T>
{
 bool operator==(T const& l, T const& r);
}

Ahora podríamos agregar tipos que cumplen ese concepto mediante lo que se denomina una asignación de concepto (concept_map).

concept_map Eq<int>
{
 bool operator==(int const& l, int const& r) { return l==r; }
}

Hay que notar que el == de los enteros no es inmediatamente Eq ya que el concepto Eq podría significar algo más que "tiene igual". Podría significar "tiene igual y es reflexivo, simétrico y transitivo" por lo que sólo el programador sabe si puede o no hacer un concept_map en función de la semántica del concepto.

La idea es que ahora, en un template, se pueda usar el igual del concepto en vez del igual de la clausura dinámica.

template<Eq T>
T f(T const& a, T const& b)
{
 return a==b ? a : b;
}

El == este sería el del concepto Eq y no una función que haya que esperar a tener el tipo T para encontrarla en tiempo de instanciación. Por tanto, este template se podría comprobar estáticamente en declaración. Sin saber si T es un int u otra cosa.

La manera de implementar algo parecido en el C++ actual (aunque sin la ventaja de la comprobación en declaración) es mediante el uso de traits. En vez de tener Eq como un concepto (ya que el C++ actual carece de ellos) lo tendremos como una clase Eq<T> que significa "el tipo T es del concepto Eq". Para que el tipo T sea del concepto Eq deberá demostrarse que el operador == es parte de ese tipo.

template<typename T>
class Eq
{
public:
static bool operator==(T const& l, T const& r);
};

El concept_map no es más que una especialización demostrando que int se ajusta a lo requerido. El compilador de C++ actual no lo comprueba así que hemos de ser cuidadosos.

template<>
class Eq<int>
{
public:
static bool operator==(int const& l, int const& r) { return l==r; }
};

Finalemente, cuando usemos estos sucedáneos de conceptos, hay que usar la clase trait en vez de utilizar directamente el operador ==. En el caso de los conceptos puros el uso del == del concepto era implícito.

template<typename T>
void f(T a, T b)
{
return Eq<T>::operator ==(a, b) ? a : b;
}



Lo importante aquí es que es posible mediante esta técnica el separar la comprobación del template de su instanciación. Desafortunadamente, este conocimiento ha llegado a la comunidad de C++ unos veinte años después de que se estandarizaran los templates (como ya he comentado en una entrada anterior, incorrectamente). Aunque usemos Eq<T>::operator ==(a, b), el compilador tomará esta expresión como dependiente y no la comprobará estáticamente.

Extensión hipotética de C++

Esto no quiere decir que no se puedan introducir. Sólo quiere decir que hay que tener muchísimo más cuidado en cómo se hace puesto que interacciona con otras características del C++ de maneras indeseadas lo cual llevó a retirar los conceptos del estándar actual.

Una manera posible de introducirlo, pienso, sería subsanando las dos carencias que hemos comentado arriba.
  1. Comprobar que las especializaciones implementan el template general. 
  2. Evitar que se tomen como expresiones dependientes las que usen estos templates comprobados. 

Postulemos entonces una pequeña variación en C++ introduciendo unos templates cuyas especializaciones tengan que implementar su interfaz forzosamente. Usaremos la hipotética sintaxis abstract template.


abstract template<typename T>
class Eq
{
public:
static bool operator==(T const& l, T const& r);
};

Con esta hipotética sintaxis el compilador se quejaría de especializaciones como estas:

//ERROR: Eq<T> es abstract y su especialización Eq<int> no implementa ==
template<> class Eq<int>
{
public:
 static bool operator<(int const& l, int const& r) { return l<r; }
};

//ERROR: El segundo argumento de operator == no es de tipo adecuado.
template<> class Eq<int>
{
public:
 static bool operator==(int const& l, float const& r) { return l<r; }
};

Como el compilador sólo va a permitir las especializaciones que cumplan con el abstract template, tiene suficiente información para comprobar las expresiones que usen este abstract template, hayan sido especializadas o no. Por esta razón no es necesario que el uso de los abstract template sean dependientes.


template<typename T>
void f(T a, T b)
{
return Eq<T>::operator ==(a, b) ? a : b;
}


La expresión Eq<T>::operator ==(a, b), aunque depende de T, como lo hace a través de un abstract template, no es dependiente y se puede comprobar ahora. De hecho, se comprueba que operator== es miembro de Eq y tiene el tipo adecuado, por tanto, la función template f() es correcta. ¡Y lo sabemos antes de instanciar T!

Estas modificaciones que propongo son en principio seguras ya que si no introduces ningún abstract template todo sigue siendo compatible. No rompe ningún código. Son fáciles de realizar puesto que es añadir una pequeña regla en la definición de expresión dependiente y la comprobación de que se implementa una interfaz es bastante parecida a la de las funciones virtuales puras (aunque hay detalles y en los detalles está el diablo).

Otra cosa es que no cumpla todos los objetivos que se esperaban de los conceptos como su uso implícito. Vayamos por partes. Esto es un primer paso, veremos como solucionar el siguiente en otro momento.

Nota: Podríamos recurrir a la vinculación dinámica en vez de a un trait y tener

template<typename T>
class Eq
{
public:
 bool (*operator==)(T const& l, T const& r); //Sin static
};

Que se usaría así

template<typename T>
void f(Eq<T>& demo, T a, T b)
{
return demo.operator ==(a, b) ? a : b;
}

Ahora bien, ¿para qué retardar la decisión a tiempo de ejecución? Muy sencillo. Este cambio permite compilar el template y generar código. Esto es lo que se llama compilación separada y aceleraría los tiempos de compilación. Como siempre, hay un compromiso entre perder algo de tiempo de ejecución o perder algo de tiempo de compilación.

miércoles, 1 de diciembre de 2010

Expresiones dependientes en templates de C++

El código

Existe un aspecto poco conocido a la hora de usar los templates de C++ que son las expresiones dependientes. Éstas dan un susto de vez en cuando a quienes las usan ya que son una mala implementación de las clausuras estáticas. El código ofensivo es el siguiente.

void f() { std::cout<<"A"; }

template<typename T>
struct B {
 void f() { std::cout<<"B"; }
};

template<typename T>
struct D : public B<T> {
 void g()
 {
  f();  //¿Imprime "A" o "B"?
 }
};

int main()
{
 D<char>::g();
}

Si llamamos a D<t>::g(), ¿se imprime "A" o se imprime "B"? Si se imprimiera "A" significa que se está usando el entorno global y si se imprime "B" significa que se está usando el entorno de la estructura D (que hereda de B). Esto está relacionado con la pregunta, ¿cuándo se compila/comprueba D? D se compila/comprueba parte cuando se declara (en el código de arriba donde pone template<typename T> struct D hasta la llave de cierre }; ) y parte cuando se instancia (cuando se hace T=char en D<char>::g() ).

Las clausuras 

Teniendo en cuenta que main() está en el entorno global y la estructura D es el entorno local de la definición de g(), el problema aquí es distinguir entre la clausura dinámica o la clausura estática. La clausura dinámica significa que si no encontramos la definición de un nombre en una declaración, buscamos en el contexto donde se está usando esa declaración (en D<char>::g()) . La clausura estática busca en el contexto donde se está declarando la declaración (en template...).

En los lenguajes basados en entorno, como el LISP o el Scheme, se usa la clausura estática, también llamada clausura léxica. Esto impide comprobar con facilidad si nuestra declaración es correcta porque no tenemos aún los tipos a los que vamos a instanciar (el char). Claro que esto no es problema ni en LISP ni en Scheme porque son lenguajes dinámicos.

La decisión 

La "solución" de los creadores de C++ fue separar la declaración del template en dos partes: la parte dependiente de los parámetros de tipos (los T) y la parte independiente de los mismos. La parte independiente puede comprobarse estáticamente, mientras que la parte dependiente se deja para cuando se conozca el argumento de tipo para la instanciación (el char).

El gran problema es que comprobar significa también buscar las declaraciones de los nombres y, en el caso de arriba, según f() sea del contexto estático o dinámico será la que imprime "A" o imprime "B".

Es fácil ver que la herencia de la clase D es B<T> y, efectivamente, depende de T por lo que no se va a buscar ningún nombre hasta el momento de instanciación ( D<char>::g() ). Antes de la instanciación está la declaración ( desde template<typename T> struct D hasta la llave de cierre }; ) y resulta que el f() que hay dentro de la declaración de g() no depende de T. Eso significa que se busca el nombre ahí, en declaración y el que encuentra es el que imprime "A" ya que el que imprime "B" depende de T a través de la herencia y no lo va a ver hasta la instanciación.

El resultado, contraintuitivo, es que imprime "A". Si tu compilador imprime "B", no se ajusta al estándar. Puedes ver más de esto en el FAQ del C++.

La historia se repite

Todo el lío fue la decisión, desde mi punto de vista errónea, de intentar comprobar el template en declaración, antes de tener los argumentos de tipos para instanciar. En el 90% de los casos al final no puedes comprobarlo en declaración porque casi todo va a depender de T, ¡que para algo se ha puesto! Así que la ganancia de separar las expresiones en dependientes e independientes es mínima y la pena es que se ha introducido la clausura dinámica por la puerta de atrás. La clausura dinámica es recordada amargamente por los programadores de LISP ya que hizo perder veinte años de investigación probando cosas como las FEXPR que al final tuvieron que ser descartadas.

In modern terminology, lexical scoping was wanted, and dynamic scoping was obtained (John McCarthy, History of Lisp)

Si se hubiera usado la clausura léxica, estos problemas no habrían surgido ya que se habría seguido la idea de sustitución (de hecho, esa fue la razón por la que se inventó la clausura léxica) y los costes habrían sido prácticamente los mismos que los que pagamos ahora.



El problema es que ahora estamos atados. De eso hablaré en otra entrada.

miércoles, 10 de noviembre de 2010

Asincronía en C# 5.0

En octubre Eric Lippert empezó una serie en su blog dedicada a la nueva característica de C#: la posibilidad de escribir programas asíncronos. Es un paso lógico tras los cambios producidos en las versiones anteriores de C#.

¿Qué tienen en común las siguientes características de C#? (Todas convenientemente explicadas por Eric)
  1. Los iteradores
  2. Métodos anónimos
  3. LINQ
  4. Asincronía
Muy sencillo. Todas estas características se obtienen mediante una transformación sintáctica del código. Es decir. No hay una nueva semántica, sólo transformaciones sintácticas. ¿Y por qué es esto relevante? Porque se habrían evitado todo el follón si hubieran introducido un mecanismo de macros en el lenguaje. No algo tan crudo como el preprocesador de C. Quizás algo como los patrones de C++. O, mucho mejor, algo como las macros del LISP o Scheme.

Si es que al final volvemos al principio.

domingo, 18 de julio de 2010

Selección con el preprocesador

Después de hablar de las macros hace unos días, me he dedicado a investigar algo más sobre cómo funciona el preprocesador de C/C++. Está muy limitado, pero tiene algunas posibilidades.

Usando #if/#endif  es posible seleccionar parte de código, pero no podemos usar #if/#endif dentro de una macro. Es decir algo como esto no funciona

#define SELECCIONA(a,b,c)  #if a \

b \

#else \

c \

#endif

El motivo es que el preprocesador es muy tonto y sus comandos (que empiezan por #) sólo son reconocidos a principio de línea. Debido a que el \ a final de línea significa que se sigue en la siguiente línea como si fuera esta, la macro anterior sería equivalente a

#define SELECCIONA(a,b,c)  #if a b #else c #endif

Por lo que ni el #if ni el #else ni el #endif son reconocidos como comandos de preprocesador.

La solución es algo enrevesada, pero funciona y es usada en boost::preprocesor. Se basa en el concatenador de tokens. Es un operador especial (no es un comando) del preprocesador. Se escribe ##. El resultado de esta concatenación es agrupar dos identificadores en uno.

#define CONCATENA(a,b) a ## b

CONCATENA(hola, adios) //Equivalente a escribir    holaadios

Debido a que el preprocesador vuelve a expandir el resultado de sus macros, es fácil usar el siguiente truco para la selección.

#define IF_TRUE(t,e) t

#define IF_FALSE(t,e) e

#define IF(c,t,e) IF_##c (t, e)

IF(TRUE, bien, mal) //Se expande a IF_TRUE(bien,mal) y se vuelve a expandir a  bien

Ahora podemos combinar este truco con la técnica explicada hace unos días para crear código opcionalmente.

martes, 29 de junio de 2010

Macros como ligazón semántica

Existen multitud de ocasiones en los cuales hay que sincronizar dos (o más) partes de un mismo programa. Generalmente los lenguajes de programación actuales no permiten expresar este tipo de invariantes fácilmente. Sin embargo, existe un pequeño truco para hacerlo mediante macros.

El problema

Supongamos que tenemos una enumeración

enum E { A, B, C };

Y queremos tener una lista con los nombres de los elementos de la enumeración.

static char *N[] = {"A", "B", "C"};

¿Qué pasa si años después en un ciclo de mantenimiento hay que añadir un nuevo elemento a la enumeración?

enum E { A, D, B, C };

Existe la posibilidad de que nos olvidemos de actualizar la lista de nombres. Nos encontramos con que el elemento D ahora tiene nombre "B". Es un error que se detectará (o no) ejecutando el programa y probándolo.

La solución

Lo que tenemos que hacer es tener una única lista y derivar las otras dos a partir de esta primera. La dificultad reside en que los elementos del lenguaje de programación que estamos usando son muy distintos entre sí: por un lado símbolos y por otro cadenas literales.

La única forma de actuar es mediante un sistema que trabaje indiferentemente sobre ambos tipos de elementos. Ese sistema en muchos lenguajes es el preprocesador de macros.

#define VALORES X(A, "A") X(B, "B") X(C, "C")

Ahora transformamos esos valores definiendo la macro X a lo que queramos (no nos olvidemos de la coma).

#define X(x, y) x,
enum E { VALORES };
#undef X

El preprocesador primero sustituirá VALORES por toda la lista

#define X(x, y) x,
enum E { X(A, "A") X(B, "B") X(C, "C") };
#undef X

Luego verá que aún tiene X por evaluar y lo hará:

#define X(x, y) x,
enum E { A, B, C, };
#undef X

Luego podrá descartar X ya que no se usa.

enum E { A, B, C, };

Casi todos los lenguajes de programación permiten una coma terminal para facilitar la construcción de herramientas que generen código fuente automáticamente. Como en este caso. Esta coma terminal se ignora por lo que al final tenemos lo que queríamos.

enum E { A, B, C };

Conclusiones

Es inmediato ver que si en vez de usar esa definición de X hubiéramos usado otra, tendríamos nuestra lista de nombres.

#define X(x, y) y,
static char *N[] = { VALORES };
#undef X

También es inmediato comprobar que hemos ligado la generación de la enumeración con la generación de la lista. De esta forma, si modificamos nuestros valores, automáticamente se modificarán ambas partes del código.

#define VALORES X(A, "A") X(D, "D") X(B, "B") X(C, "C")

Es justo lo que queríamos: ligar semanticamente dos partes del código de forma automática. Ahora es imposible que nos confundamos dándole a D el nombre "B" como ocurría antes.

Referencias: Walter Bright - The X Macro

miércoles, 4 de noviembre de 2009

Templates y Generics

Programación genérica

La programación genérica se basa en el uso de tipos paramétricos. Esto se consigue diciendo cosas como "una lista de enteros" o "una lista de cadenas". De forma usual esto se escribe

list<int>

list<string>

Generics

Ahora bien, el significado de estos tipos puede ser distinto. En Java (y en parte en C#) lo que se hace es borrar el tipo y usar el tipo objeto.

list<int> ----> list<object>

list<string> -----> list<object>

A cambio, el compilador comprueba los tipos e introduce las conversiones convenientes en cada caso. De esta forma hay seguridad en el uso de los tipos aunque realmente estemos trabajando sobre objetos genéricos. De ahí el nombre de estos tipos: tipos genéricos (generics).

Templates

En C++ los tipos no se borran, lo que se hace es generar un tipo nuevo a partir de un patrón. Así una lista de algo no es un tipo completo en C++ es un patrón. De ahí el nombre: patrones (templates).

list<X> ----> class { void insert(X x) {..} ..}

list<int> ----> class { void insert(int x) {..} ..}

list<string> ----> class { void insert(string x) {..} ..}

Obviamente esto es mucho más pesado para el compilador que requiere crear estos tipos. Por esta razón las compilaciones de C++ son mucho más largas que las de C# o las de Java. Realmente son macros que expanden el código a compilar.

Comparativa

Esta diferencia hace que existan casos no intuitivos. Por ejemplo, en Fabulous Adventures In Coding Eric Lippert nos muestra el siguiente ejemplo en C#.

public class C

{

public static void DoIt<T>(T t) {ReallyDoIt(t);}

private static void ReallyDoIt(string s)

  {System.Console.WriteLine("string");}

private static void ReallyDoIt<T>(T t)

  {System.Console.WriteLine("everything else");}

}

Si llamamos a C.DoIt<string> el compilador de C# borra el tipo (generics, no templates) y cuando llama a ReallyDoIt(t) es un object por lo que el resultado es "everything else".


miércoles, 28 de octubre de 2009

Macros vs Funciones

Recapitulando

En el último post dedicado a las macros dije que:

En general queremos proteger los símbolos. Una macro debe ser higiénica y transparente por defecto.

Si queremos definir un símbolo en el entorno de llamada, querremos que un símbolo sea no-higiénico. Usualmente este símbolo deberá ser pasado por argumento para controlar qué se ensucia en el entorno de llamada.

Si queremos usar un símbolo del entorno de llamada, querremos que un símbolo sea opaco. Usualmente este símbolo no deberá ser pasado por argumento porque es posible que ni siquiera exista (caso Windows/Linux).

Esto es exactamente lo mismo que hacemos con funciones. La salvedad es que en las funciones trabajamos con valores (en tiempo de ejecución) mientras que en macros trabajamos con símbolos (en tiempo de compilación). Obviamente, no existe esta distinción en lenguajes homoicónicos como LISP donde la compilación y la ejecución van a la par.

Traducción

Esto significa que existen los mismos conceptos de higiene y transparencia referencial en las funciones y, además, existen unos motivos muy parecidos para tener excepciones a la regla.

Por ejemplo:

En general queremos proteger los símbolos. Una macro debe ser higiénica y transparente por defecto.

En general queremos proteger los valores. Una función debe ser higiénica y transparente por defecto.

Realmente esto es cierto, aunque es muy fácil perder la transparencia en lenguajes como el C/C++. Lo veremos más abajo.

Si queremos definir un símbolo en el entorno de llamada, querremos que un símbolo sea no-higiénico. Usualmente este símbolo deberá ser pasado por argumento para controlar qué se ensucia en el entorno de llamada.

Si queremos modificar un valor en el entorno de llamada, querremos que un valor sea no-higiénico. Usualmente este valor deberá ser pasado por referencia para controlar qué se modifica en el entorno de llamada.

Así pues, opino que existe una analogía entre la higiene y el paso por valor. La no-higiene y el paso por referencia.

Si queremos usar un símbolo del entorno de llamada, querremos que un símbolo sea opaco. Usualmente este símbolo no deberá ser pasado por argumento porque es posible que ni siquiera exista (caso Windows/Linux).

Si queremos usar un valor del entorno de llamada, querremos que ese valor sea accesible. Usualmente este valor no deberá ser pasado por argumento porque es posible que ya podamos acceder a él.

El ejemplo claro es una variable global. Al ser global está en el entorno de llamada. Además, ya podemos acceder a ella por lo que no necesitamos pasarla por argumento. De hecho, es esto lo que hace que la función no sea transparente. Depende de algo que no pasamos por argumentos. Esto es muy común en C/C++ donde hay variables globales, estáticas y argumentos implícitos.

domingo, 25 de octubre de 2009

Transparencia en tu macro

Flashback

Vamos a volver atrás en el tiempo a la entrada de Higiene en tu macro. En un punto del texto decía...

Como ejemplo, una macro m definida así:

macro m :=
{
y=3; //Defino la y dentro del código.
f(z);
}


Y usada en un código así:

z=7;
m;
print(y); //Debería dar error, no he definido ninguna y en este código.

Da este resultado:

z=7;
y=3; //Contamino el entorno de fuera desde la macro.
f(z); //Uso la z de fuera en la macro ¿es esto deseable?.
print(y); //Captura la y de la macro en vez de estar sin definir.

Es completamente antihigiénico porque introducimos el símbolo y en el entorno de uso.

En aquella entrada nos dedicamos a la higiene. Es decir, que los símbolos de dentro de la macro no se escapasen fuera. Ahora vamos a pensar en la transparencia referencial: que los símbolos de fuera no me afecten dentro.

Transparencia referencial

Realmente la transparencia referencial es más que los símbolos de fuera no afecten dentro. Significa que nada de lo de afuera va a afectar la macro (igual con funciónes). Por tanto, la macro (o función) sólo podrá depender de los argumentos que se le pasen. Exactamente como una función matemática.

Para saber si una macro (o función) f() es transparente o no, hacemos el siguiente experimento mental:

  • Llamo a f() con un valor concreto. Por ejemplo, f(3). Obtengo un resultado R1.
  • Supongo que se ejecuta código arbitrario entre medias.
  • Llamo a f() con el mismo valor concreto. Por ejemplo, f(3). Obtengo ahora un resultado R2.
  • ¿Existe la posibilidad de que el resultado R1 sea distinto al resultado R2?

Si la respuesta es "sí", no es transparente.

En general, queremos transparencia porque nos permite razonar con mayor facilidad sobre lo que hace una macro (o función) El sistema de entornos expuesto en el post de la higiene es automáticamente transparente (gracias al entorno padre=entorno de definición). Ahora bien, ¿es esto deseable?

Deseable opacidad

Si no tenemos transparencia de ningún tipo, cualquier operación que hagamos dentro de una macro (o función) tendrá un significado desconocido. Supongamos

macro m := { y=x*x }

Parece que es hacer y el cuadrado de x. Ahora bien, podría darse el caso de que el uso de m apareciera en el siguiente entorno.

defino * como +

m //Uso la macro

¡Ahora y no es el cuadrado es el doble! Así pues, la opacidad referencial implica usar símbolos cuyo valor o definición puede realizarse luego. Esto es muy peligroso, pero puede ser muy útil.

Imaginemos el siguiente ejemplo donde defino la macro m con dos parámetros.

macro delay(os, ms) :=

{

 static_if(os==windows) Sleep(ms)

 else static_if(os==linux) usleep(ms*1000)

 else error;

}

Para empezar es una macro higiénica (ni el os ni el ms que defino en el entorno local podrán usarse fuera de la macro). Como es macro, se ejecuta en tiempo de compilación. Esto se resalta con el uso de static_if en vez de if a secas. El static_if no genera código, simplemente es como si no se escribiera el código en la rama que no se toma. En ese sentido es como el #if del preprocesador de C/C++.

Para continuar, supongamos transparencia referencial para la macro. Eso quiere decir que todos los símbolos han de tomarse del entorno de definición ¡y eso es un problema! Porque si estamos en Windows, usleep no estará definido y será un error y, si estamos en Linux, Sleep no se encontrará y será un error.

Es decir, que esta macro necesita ser opaca. Pero sólo con con los símbolos Sleep y usleep. No con el símbolo static_if ni con el * que queremos que estén vinculados con los valores del entorno de definición.

Resumiendo

En general queremos proteger los símbolos. Una macro debe ser higiénica y transparente por defecto.

Si queremos definir un símbolo en el entorno de llamada, querremos que un símbolo sea no-higiénico. Usualmente este símbolo deberá ser pasado por argumento para controlar qué se ensucia en el entorno de llamada.

Si queremos usar un símbolo del entorno de llamada, querremos que un símbolo sea opaco. Usualmente este símbolo no deberá ser pasado por argumento porque es posible que ni siquiera exista (caso Windows/Linux).

viernes, 21 de agosto de 2009

Higiene en tu macro

El modelo de entornos

A la hora de sustituir nombres de variables por valores hay dos formas de proceder. La primera es el modelo de sustituciones propio del lambda-cálculo. Si x vale 3 cambio la x por el 3. Este modelo es válido cuando la x siempre vale 3. Si quiero que la x valga otra cosa necesito saber cuándo vale 3 y cuándo vale otra cosa.

En este punto entra la idea de entorno. Un entorno no es más que una serie de nombres con sus valores correspondientes. Podría ser algo como

x=3
y="hola"
z=λx.f(x)

En la variable z hay una cosa interesante. ¡Hay una x! Y la x valía 3... a no ser que esté en otro entorno. Eso es lo que ocurre. El lambda-cálculo se integra en el modelo de entornos haciendo que las abstracciones lambda tengan un pequeñito entorno dentro de ellas. Este entorno local se crea cuando se aplica la abstracción. Al calcular

z(7)

lo que ocurre es que creo un entorno nuevo donde los parámetros (la x) se vinculan a los argumentos (el 7)

x=7

Y, entonces, sustituyo:

z(7)=f(7)

Ahora tenemos otro problema: ¡f no está en ninguno de nuestros entornos! Cierto. Debe existir otro entorno donde esté f definida y ese entorno debe estar visible desde el entorno local de la lambda. Así pues, cada entorno puede tener un entorno padre donde busque los nombres que no encuentre en él mismo.

Los tres entornos, las funciones

Existen tres entornos que participan a la hora de calcular el valor de una función. Son:

  • El entorno de uso. Es decir, donde se realiza la aplicación de la función con sus argumentos.
  • El entorno local. Es ese donde se vinculan los argumentos a los parámetros. Es temporal, en cuanto se acabe de calcular la función, se olvida.
  • El entorno de definición. Es donde se creó la función, la abstracción lambda. Es donde está la f del ejemplo anterior.

En una función, el entorno padre del entorno local es el entorno de definición. Esto es lo que se llama la clausura léxica. Muchos lenguajes no soportan que un entorno tenga padre y, por tanto, usan los nombres de un entorno global.

El entorno local protege el entorno de uso de agresiones. Si una función tiene que usar un nombre para un cálculo parcial, ese nombre estará en el entorno local y morirá cuando la función termine de calcular. Si eso no fuera así tendríamos el siguiente problema.

funcion f(x) :=
{
 //Creo aquí un nombre que debería ser
 //temporal y oculto en f(x)
 y=x*x;
 retorna 2*y;
}
//Creo aquí un nombre que no tiene nada que ver
//con el anterior y porque estamos en otro entorno
y=7;
f(4);
//¿Cuánto vale aquí y?

Si y vale 7 significa que hemos protegido la y de fuera de f(x) de la y de dentro que vale 4*4=16.

Y las macros higiénicas

El no proteger estos símbolos da origen a lo que se llama falta de higiene porque ensuciamos los símbolos de fuera. Es una característica de los sistemas de macro tradicionales. Una macro es un conjunto de expresiones que se ponen, sin más miramientos, donde aparece el nombre de una macro.

Como ejemplo, una macro m definida así:

macro m :=
{
y=3; //Defino la y dentro del código.
f(z);
}

Y usada en un código así:

z=7;
m;
print(y); //Debería dar error, no he definido ninguna y en este código.

Da este resultado:

z=7;
y=3; //Contamino el entorno de fuera desde la macro.
f(z); //Uso la z de fuera en la macro ¿es esto deseable?.

print(y); //Captura la y de la macro en vez de estar sin definir.

Es completamente antihigiénico porque introducimos el símbolo y en el entorno de uso.

Las macros higiénicas evitan eso usando, como las funciones, un entorno local. Sería entonces algo parecido a:

z=7;
{
y=3; //No contamino el entorno de fuera
f(z); //¡¡No es una función porque puedo usar la z!!

}
print(y);

Las macros higiénicas pueden verse como una función en el que el entorno padre del entorno local no es el entorno de definición, sino el de uso.