sábado, 2 de febrero de 2013

Coma serial, agujeros de gusano, gotos desnudos, e implícitos por tipo

Funciones

Llamemos a una función. Una función que está en un lugar distante del código.
Si sigo el flujo de control (dibujado en rojo abajo) y el flujo de datos (en azul), ambos van paralelos. Es decir, el valor de a (dato) se lleva a la función doble (control) y se trae de vuelta el resultado (dato) al retornar (control).
La variable a es local. Eso quiere decir que fuera de mi trocito de código no se ve. Por eso necesitamos pasar la variable a a la función doble. ¿Qué pasaría si a fuera global y sí se viese en todo el programa incluida la función doble?

Variables globales

En este caso, no hace falta pasar la variable global a ninguna función, puesto que ya se ve desde allí.
Ahora, una vez guardado el dato 3 en la variable global a, está disponible en cualquier punto del programa. No hace falta pasarlo como argumento a la función doble. El flujo de datos a través de la variable global a se ha ocultado.

Ahora bien, ¿qué ocurre si otro trozo de código ejecuta doble en vez del que estoy usando como ejemplo?
Primero, el flujo de control llegará y asignará el dato 3 a la variable global a. Luego, el programa seguirá funcionando. Algún tiempo o mucho tiempo después, el flujo de control llegará al trozo de código en el que se ejecuta doble y se recupera el valor de la variable global a.

La variable global ha hecho que el dato 3 se pase de una punta a la otra del programa sin que haya relación directa entre esos dos trozos de código. Si no vemos la definición de doble, el uso de la variable global a dentro de doble no es tan obvio. No aparece ni en sus parámetros ni en sus argumentos. El efecto es el de un dato que se teleporta de un lugar a otro del programa.

Beam me up, Scotty!


Entra en un lugar del universo y sale por otro completamente distinto. Como los agujeros de gusano, pero mucho peor ya que puede haber más de dos extremos en una variable global.

Que alguien busque un error en un programa lleno de variables globales con datos que se teleportan de un lado a otro sin relación directa es muy, muy difícil.

Conclusión lógica: mejor evitar el uso de variables globales porque separan el flujo de datos del flujo de control.

Se considera al goto dañino

Hay ríos de tinta (y de bits) explicando que la instrucción goto debe ser evitada a toda costa y, además, su uso es síntoma de mal código. Muchas veces se toma demasiado al pie de la letra el título del artículo de Dijkstra que puso este problema sobre la mesa, pero sin terminar de leer el artículo por completo y viendo el porqué de la mala fama del goto.

Realmente no siempre el goto es malo. Veamos la razón. El goto es el caso complementario a una variable global. En la variable global era el flujo de datos el que saltaba de una punta a otra del código, con el goto es el flujo de control el que salta.
Como goto no permite pasar datos al lugar donde salta, el flujo de datos se acaba en el goto y empieza en el lugar donde llega. Este lugar es llamado la etiqueta. Llamaremos a este tipo de goto un goto desnudo.

¿Y si quiero pasar datos desde un goto desnudo hasta su etiqueta? Muy sencillo: debemos usar las variables globales.
Es más, si usamos gotos desnudos, no nos queda otra opción que usar variables globales. Las variables globales implican saltos en el flujo de datos: lo que entra en una variable global puede aparecer en la otra punta del código. Además, los gotos implican saltos en el flujo de control: la etiqueta de destino de un goto puede estar en la otra punta del código. Las dos cosas juntas hacen que la comprensión de un programa no trivial sea imposible.

Conclusión: los gotos desnudos son malos porque nos fuerzan a usar variables globales. Realmente, la raíz de todo mal está en las variables globales ya que los gotos pueden modificarse para que no sea necesario el uso de estas variables.

Restricción de saltos

¿Cómo podemos restringir el uso de los goto para que sea seguro? La primera lección la da el lenguaje C: un goto sólo puede saltar a una etiqueta que esté en su misma función.

void g(void)
{
y:
  puts(“ups!”);
}

void f(void)
{
  int a;
x:
  a=rand();
  if(a==0) goto x;  //OK: x está en f
  else     goto y;  //ERROR: y está en otra función
  printf(“%d\n”,a);
}

De esta forma antes y después del goto están visibles las variables locales de la función. No hace falta usar variables globales ya que la comunicación se realiza mediante variables locales.

Otra lección proviene de la teoría de lenguajes y consiste en añadir al goto un parámetro. Llamaremos a este goto vestido.

goto x(3);

Y en la etiqueta recogemos el argumento.

x(a):
print(a);

La etiqueta se comporta en cierta manera como una función que nunca retorna. Esto se acerca al concepto de continuación. También, con las continuaciones, pasamos los datos como si fuera una función y no necesitamos variables globales.

Evitamos la raíz de todo mal: las variables globales y sus agujeros de gusano.

Nota: Una continuación es un valor de forma similar a lo que las clausuras son a las funciones, por lo que el goto vestido no es exactamente una continuación.

Implícitos

Sin embargo, no tener variables globales es tedioso a la hora de escribir un programa porque hay que ir arrastrando el contexto que antes mantenía la variable global.

Propongamos este simple ejemplo:

Dato d; //Mi dato d que voy a usar como variable global

void h(Dato d); //La función que usa el dato
void g(OtrosDatos a, YMasDatos b)
{
  //Esta función hace otras cosas con a y b pero, al final usa h
  h(d)
}
void f(AlgunasCosas x, OtrasCosas y)
{
  //Esta función hace cosas con x e y, pero al final llama a g
  g(a,b)
}
int main()
{
  //Prepara x e y
  //Preparo mi dato d
  f(x,y)
}

La función h usa d. Por ese motivo se dice que d es contexto de h. Como d es global no tenemos problemas en escribirla en cualquier parte del código, incluida h. ¿Qué pasaría si d no pudiera ser global? Tendríamos que ponerla en main(), pasársela a f y propagarla hasta llegar a h. Es decir, tenemos que escribir el contexto en los parámetros de las funciones que lo usan.

Esto es justamente lo que deseamos hacer: que el flujo de datos y de control vayan paralelos.

void h(Dato d); //La función que usa el dato
void g(OtrosDatos a, YMasDatos b, Dato d)
{
  //Esta función hace otras cosas con a y b pero, al final usa h
  h(d)
}
void f(AlgunasCosas x, OtrasCosas y, Dato d)
{
  //Esta función hace cosas con x e y, pero al final llama a g
  g(a,b,d)
}
int main()
{
  //Prepara x e y
  Dato d; //Mi dato d que voy a usar como variable local
  //Preparo mi dato d
  f(x,y,d)
}

En las funciones f y g hemos tenido que añadir el dato para transmitirlo hasta la función h. Cada vez que usemos f o g, hemos de añadir d ¡y eso pueden ser muchas, muchas veces! ¿Cómo solucionarlo sin variables globales?

La mejor forma que he visto es mediante argumentos implícitos. Una característica del lenguaje de programación Scala. Consiste en añadir la palabra clave implicit en los parámetros que se comportan como propagadores de contexto.

void h(Dato d); //La función que usa el dato
void g(OtrosDatos a, YMasDatos b, implicit Dato d)
{
  //Esta función hace otras cosas con a y b pero, al final usa h
  h(d)
}
void f(AlgunasCosas x, OtrasCosas y, implicit Dato d)
{
  //Esta función hace cosas con x e y, pero al final llama a g
  g(a,b,d)
}
int main()
{
  //Prepara x e y
  Dato d; //Mi dato d que voy a usar como variable local
  //Preparo mi dato d
  f(x,y,d)
}

Hasta ahora nada cambia, pero declarar un parámetro como implícito significa que es opcional y, si no se pone, se busca el llamado valor implícito de ese tipo de datos en el entorno actual. La forma de declarar un valor implícito es también con la palabra clave implicit.

void h(Dato d); //La función que usa el dato
void g(OtrosDatos a, YMasDatos b, implicit Dato d)
{
  //Esta función hace otras cosas con a y b pero, al final usa h
  h(d)
}
void f(AlgunasCosas x, OtrasCosas y, implicit Dato d)
{
  //Esta función hace cosas con x e y, pero al final llama a g
  g(a,b,d)
}
int main()
{
  //Prepara x e y
  Dato d; //Mi dato d que voy a usar como variable local
  //Preparo mi dato d
  implicit d; //Usa d como el valor implícito para Dato
  f(x,y) //El implícito para el tipo Dato es d.
         //Entonces, es como llamar a f(x,y,d)
}

Nota: ¿Por qué Scala asocia un valor implícito al tipo Dato y no, por ejemplo, a cada nombre de manera que si no escribo el argumento implícito busque la variable con nombre “d”? ¿No es muy rebuscado buscar el valor implícito según sea el tipo del parámetro? La respuesta es bastante teórica. Este mecanismo introduce una dependencia de valores desde los tipos y esto es muy útil para otras muchas cosas como los conceptos (también llamados clases de tipos).

Lo interesante es que un parámetro implícito es directamente el valor implícito en el entorno de la función, por lo que tampoco hace falta escribir g(a,b,d).

void h(Dato d); //La función que usa el dato
void g(OtrosDatos a, YMasDatos b, implicit Dato d)
{
  //Esta función hace otras cosas con a y b pero, al final usa h
  h(d)
}
void f(AlgunasCosas x, OtrasCosas y, implicit Dato d)
{
  //Esta función hace cosas con x e y, pero al final llama a g
  g(a,b) //d es implícito aquí también
}
int main()
{
  //Prepara x e y
  Dato d; //Mi dato d que voy a usar como variable local
  //Preparo mi dato d
  implicit d; //Usa d como un implícito para Dato
  f(x,y) //El implícito para el tipo Dato es d.
         //Entonces, es como llamar a f(x,y,d)
}

Por supuesto, en este ejemplo sólo nos hemos ahorrado escribir d dos veces, pero en un programa real suele haber muchos más usos de este tipo de parámetros. Así que con estos parámetros implícitos se soslaya el inconveniente de no poder usar variables globales.

La coma serial

Una bagatela para finalizar. El título de esta entrada se llama "Coma serial, agujeros de gusano, gotos desnudos, e implícitos por tipo". Un lector atento habrá visto una escritura inusual de la enumeración. Concretamente usando la coma antes de la conjunción.

Este tipo de uso de la coma no es común en el español, pero en inglés sí que lo es. Se denomina la coma de Oxford, la coma de Harvard o la coma serial. Se usa para evitar ambigüedades como estas:

"Dedicado a mis padres, Juan y Ana." Bien. Los padres son Juan y Ana.
"Dedicado a mis padres, Juan, y Ana." Mmmm.... Los padres no son Juan y Ana. ¿Verdad?

En el caso del título podía confundirse "e implícitos por tipo " como si los goto fueran, además de desnudos, implícitos por tipo. No es el caso. Los implícitos por tipo son otra cosa. Para evitar la ambigüedad, usé la coma serial.

domingo, 20 de enero de 2013

Hazlo todo. Hazlo bien. Hazlo puro. Hazlo mucho.

Leí hace poco un consejo para programar bien. Decía: haz que funcione, haz que sea correcto y haz que sea rápido. Como no todo lo que hago es programar, he pensado que se podía generalizar el consejo a otras áreas; generalizando las instrucciones. El resultado ha sido el siguiente:
  1. Hazlo todo. Si es un programa de ordenador, haz que funcione todo. Si es una canción, componla entera. Si es una historia, escríbela por completo (tercera regla de Gaiman). Si es un dibujo, acábalo.
  2. Hazlo bien. Busca los errores en el programa de ordenador y arréglalos. Busca los puntos de poca fuerza en la composición y enfatízalos. Busca los fallos de estructura en la historia y ajústalos. Busca las desproporciones en el dibujo y corrígelas.
  3. Hazlo puro. Simplifica y optimiza el programa. Haz nítida la composición. Quita escenas y diálogos inútiles en tu historia (por ejemplo, la navaja de Chekhov). Deja sólo las líneas necesarias en tu dibujo.
El proceso es el mismo. Siempre. Puede ser iterativo y que tengamos que volver del punto 3 al 1, pero al final entramos por el 1 y salimos por el 3. (Empezar por el 3 es desastroso como ya advertía Knuth.)

Finalmente, debo añadir un metaconsejo que es superior a todos los anteriores.
  • Hazlo mucho. Si eres programador, programa mucho. Si eres compositor, compón mucho. Si eres escritor, escribe mucho. Si eres dibujante, dibuja mucho.
El resultado de tu capacidad es resultado de la práctica que tengas. No hay otra opción que practicar una vez conocida la teoría. No vale saberse la teoría únicamente. Incluso la teoría matemática más pura no se comprende completamente sólo leyéndola. Hay que hacer ejercicios una y otra vez. Debes fracasar muchas veces antes de conseguir algún resultado.

viernes, 2 de marzo de 2012

Pequeña introdución al álgebra geométrica


Operaciones del cálculo vectorial y su simplificación

Supongo que el lector sabe que el cálculo vectorial añade dos operaciones a las propias de un espacio vectorial. Las operaciones propias de un espacio vectorial son el producto por un escalar ([$\lambda a$]) y la suma de vectores ([$a+b$]). Las añadidas por el cálculo vectorial son el producto escalar ([$a\cdot b$]) y el producto vectorial ([$a\times b$]).

¿Existirá alguna manera de simplificar este esquema volviendo únicamente a dos operaciones? Bueno, es fácil sobrecargar el producto para que, si uno de sus factores es escalar, se comporte como el producto por un escalar. Ahora queda pensar en cómo aglutinar el producto escalar y el vectorial. La idea básica surge de la forma en la que estos productos tratan los vectores proporcionales y ortogonales.

[$a$] y [$b_\parallel$] son proporcionales[$a\cdot b_\parallel=b_\parallel\cdot a$][$a\times b_\parallel=0$]
[$a$] y [$b_\bot$] son ortogonales[$a\cdot b_\bot=0$][$a\times b_\bot=-b_\bot\times a$]

Como todo vector puede ser descompuesto en una parte proporcional y otra ortogonal, lo que tenemos al final es que si [$b=b_\parallel+b_\bot$][$$a\cdot b=a\cdot (b_\parallel+b_\bot )=a\cdot b_\parallel+a\cdot b_\bot=a\cdot b_\parallel+0=a\cdot b_\parallel$$][$$a\times b=a\times (b_\parallel+b_\bot )=a\times b_\parallel+a\times b_\bot=0+a\times b_\bot=a\times b_\bot$$]Si de alguna manera el nuevo producto fuera similar a una suma de tanto el producto escalar como el producto vectorial, podríamos unificarlos en un mismo producto simplificado al que llamaremos producto geométrico.[$$ab=a(b_\parallel+b_\bot )=ab_\parallel+ab_\bot$$]Donde la parte [$ab_\parallel$] estará relacionada con el producto escalar y la parte [$ab_\bot$] con el producto vectorial. Lo único que hemos requerido es la propiedad distributiva que, de todas las maneras, debe tener un producto lineal.

Debido a que queremos que la parte [$ab_\parallel$] siga las propiedades del producto escalar, tendrá que ser simétrica; y debido a que queremos que la parte [$ab_\bot$] siga las propiedades del producto vectorial, tendrá que ser antisimétrica: [$$ab_\parallel=b_\parallel a$$][$$ab_\bot=-b_\bot a$$] Y, a partir de ellas llegamos a: [$$ba=(b_\parallel+b_\bot )a=b_\parallel a+b_\bot a=ab_\parallel-ab_\bot$$] Este resultado es muy interesante ya que nos va a permitir obtener la parte relacionada con el producto escalar y la parte relacionada con el producto vectorial a partir del nuevo producto geométrico. [$$ab+ba=2ab_\parallel$$][$$ab-ba=2ab_\bot$$]Esta es la idea. Sustituimos dos productos por uno del que, de la forma arriba vista, puede volver a extraerse la información de cada uno de esos dos productos iniciales.

Definiendo el producto geométrico

La forma usual que tenemos de analizar una operación en un espacio vectorial es descomponiendo los vectores como una combinación lineal de los vectores de una base. Bastará ver cómo opera el producto con estos vectores de la base.
Cuando una base [$\{e_1,e_2,e_3\}$] de un espacio vectorial [$V$] es ortonormal el producto escalar se comporta tal como se expresa en la siguiente tabla de Cayley.
[$\cdot$][$e_1$][$e_2$][$e_3$]
[$e_1$][$1$][$0$][$0$]
[$e_2$][$0$][$1$][$0$]
[$e_3$][$0$][$0$][$1$]
Esta tabla de Cayley no es más que la abreviatura de[$$e_1\cdot e_1=1,e_1\cdot e_2=0,e_1\cdot e_3=0$$][$$e_2\cdot e_1=0,e_2\cdot e_2=1,e_2\cdot e_3=0$$][$$e_3\cdot e_1=0,e_3\cdot e_2=0,e_3\cdot e_3=1$$]Por otra parte, el producto vectorial tiene los siguientes resultados para una base orientada hacia la derecha:
[$\times$][$e_1$][$e_2$][$e_3$]
[$e_1$][$0$][$e_3$][$e_2$]
[$e_2$][$-e_3$][$0$][$e_1$]
[$e_3$][$-e_2$][$-e_1$][$0$]
Es importante resaltar aquí que el [$0$] es el vector nulo y no el escalar nulo.

En el caso del producto geométrico vamos a tener una mezcla de ambos tipos de producto, con ciertas particularidades. La primera es que incluimos el [$1$] como un componente de la base, ya que queremos subsumir el producto por escalar en el producto geométrico. Esto diluye la diferencia entre el cero escalar y el cero vectorial. El resultado es la siguiente tabla de Cayley.
[$1$][$e_1$][$e_2$][$e_3$]
[$1$][$1$][$e_1$][$e_2$][$e_3$]
[$e_1$][$e_1$][$1$][$e_1 e_2$][$e_1 e_3$]
[$e_2$][$e_2$][$e_2 e_1=-e_1 e_2$][$1$][$e_2 e_3$]
[$e_3$][$e_3$][$e_3 e_1=-e_1 e_3$][$e_3 e_2=-e_2 e_3$][$1$]
Ahora tenemos que pensar qué hacemos con los valores [$e_1 e_2$], [$e_1 e_3$] y [$e_2 e_3$]. En el producto vectorial las operaciones con la forma [$e_1\times e_2=e_3$] daban como resultado un vector. Sin embargo, hacer esto con el producto geométrico nos lleva a perder la propiedad asociativa. Si [$e_1 e_2 = e_3 $] lo que tenemo es [$$e_1 (e_1 e_2 )=e_1 e_3\ne e_2=(e_1 e_1 ) e_2$$] Como la propiedad asociativa es necesaria para emular tanto el producto escalar como el vectorial, lo que vamos a hacer es tratar los valores [$e_1 e_2$], [$e_1 e_3$] y [$e_2 e_3$] como fundamentales. No se pueden simplificar. En ese sentido son como [$e_1$],[$e_2$] y [$e_3$] o, también, la propia unidad escalar [$1$]. Es decir, serán componentes de la base del espacio vectorial sobre el que va a operar el producto geométrico. Llamaremos a este espacio [$\Lambda(V)$] donde [$V$] es el espacio original.

Las signaturas del producto escalar, vectorial y geométrico son:[$$ \_\cdot\_:V\times V \to \mathbb{K}$$][$$ \_\times\_ : V\times V \to V $$][$$ \_\ \_:\Lambda(V)\times\Lambda(V)\to\Lambda(V)$$]
Ahora bien, ¿qué componentes tendrá la base de este espacio [$\Lambda(V)$]? ¿Faltará alguno más? Podemos volver a repetir el producto sobre [$e_1 e_2$] y obtener cosas como [$e_1 e_3 e_2$] o [$e_2 e_3 e_1 e_2$], lo que ocurre es que al final se simplifican o bien a [$e_1 e_2 e_3$] o a otra de las combinaciones vistas hasta ahora. Lo único que se requiere es asociatividad y la propiedad de elemento neutro escalar [$1A=A$].[$$ e_1 e_3 e_2=e_1 (e_3 e_2 )=-e_1 (e_2 e_3 )=-e_1 e_2 e_3$$][$$e_2 e_3 e_1 e_2=-e_2 e_3 e_2 e_1=e_2 e_2 e_3 e_1=1e_3 e_1=e_3 e_1$$]Así que, el nuevo espacio vectorial [$\Lambda(V)$] donde opera el producto geométrico tendrá la siguiente base.[$$\{1,e_1,e_2,e_3,e_1 e_2,e_1 e_3,e_2 e_3,e_1 e_2 e_3 \}$$] Por ejemplo: El valor [$5+4e_1-7e_1 e_3$] es un vector de este espacio. Sus coeficientes en la base de arriba serían [$(5,4,0,0,0,-7,0,0)$].

Notaremos los elementos de [$\Lambda(V)$] con letras mayúsculas [$A,B,C$] mientras que continuamos usando las minúsculas [$a,b,c$] para los elementos de [$V$] y las griegas [$\alpha, \beta, \gamma$] para los escalares.

El producto geométrico es cerrado sobre el espacio [$\Lambda(V)$] que, a su vez, contiene el espacio [$V$] como subespacio vectorial con su base [$\{e_1,e_2,e_3 \}$]. La tabla que define el producto geométrico es la que sigue, aunque no es más que lo que hemos visto hasta ahora ampliado a la base completa.
[$1$][$e_1$][$e_2$][$e_3$][$e_1 e_2$][$e_1 e_3$][$e_2 e_3$][$e_1 e_2 e_3$]
[$1$][$1$][$e_1$][$e_2$][$e_3$][$e_1 e_2$][$e_1 e_3$][$e_2 e_3$][$e_1 e_2 e_3$]
[$e_1$][$e_1$][$1$][$e_1 e_2$][$e_1 e_3$][$e_2$][$e_3$][$e_1 e_2 e_3$][$e_2 e_3$]
[$e_2$][$e_2$][$-e_1 e_2$][$1$][$e_2 e_3$][$-e_1$][$-e_1 e_2 e_3$][$e_3$][$-e_1 e_3$]
[$e_3$][$e_3$][$-e_1 e_3$][$-e_2 e_3$][$1$][$e_1 e_2 e_3$][$-e_1$][$-e_2$][$e_1 e_2$]
[$e_1 e_2$][$e_1 e_2$][$-e_2$][$e_1$][$e_1 e_2 e_3$][$-1$][$-e_2 e_3$][$e_1 e_3$][$-e_3$]
[$e_1 e_3$][$e_1 e_3$][$-e_3$][$-e_1 e_2 e_3$][$e_1$][$e_2 e_3$][$-1$][$-e_1 e_2$][$e_2$]
[$e_2 e_3$][$e_2 e_3$][$e_1 e_2 e_3$][$-e_3$][$e_2$][$-e_1 e_3$][$e_1 e_2$][$-1$][$-e_1$]
[$e_1 e_2 e_3$][$e_1 e_2 e_3$][$e_2 e_3$][$-e_1 e_3$][$e_1 e_2$][$-e_3$][$e_2$][$-e_1$][$-1$]
Esta tabla (o las propiedades vistas con anterioridad) nos va a servir para calcular el producto geométrico sobre cualquier elemento del nuevo espacio vectorial ampliado.
[$$(5+4e_1-7e_1 e_3 )(e_2+e_1 e_2 e_3 )=5e_2+5e_1 e_2 e_3+4e_1 e_2+4e_2 e_3+7e_1 e_2 e_3-7e_2=$$]
[$$=-2e_2+4e_1 e_2+4e_2 e_3+12e_1 e_2 e_3$$]
Curiosamente, el valor [$e_1 e_2 e_3$] cumple que [$(e_1 e_2 e_3 )^2=-1$]. Por similitud con la unidad imaginaria se nota con [$I=e_1 e_2 e_3$] y se le da el nombre de pseudoescalar. Si nos restringimos a vectores de [$\Lambda(V)$] de la forma [$\alpha+\beta I$], todo el cálculo complejo aparece en este subespacio.
Así que el producto geométrico no sólo aúna los productos escalares y vectoriales, sino que también los números complejos.

Sorpresas algebraicas

El producto geométrico de un vector [$a$] de [$V$] por sí mismo es un escalar, y además positivo. Obviamente, llamaremos esta multiplicación por sigo mismo, potenciar al cuadrado.[$$a^2=(\alpha_1 e_1+\alpha_2 e_2+\alpha_3 e_3 )(\alpha_1 e_1+\alpha_2 e_2+\alpha_3 e_3 )=\alpha_1^2+\alpha_2^2+\alpha_3^2$$] Eso significa que se puede definir la inversa de un vector respecto al producto geométrico. [$$a^{-1}=a/a^2$$] Se comprueba que, efectivamente, es una inversa. Basta con tener cuidado de saber que [$a^2$] es un escalar y por eso podemos calcular [$1/a^2$] en el cuerpo de [$V$].

Aunque todos los vectores no nulos de [$V$] tienen inversa, no todos los elementos del espacio completo [$\Lambda(V)$] tienen inversa. Por ejemplo, [$1+e_1$] no la tiene ya que es un divisor de cero [$(1+e_1 )(1-e_1 )=1-e_1+e_1-1=0$]. Si tuviera inversa llegaríamos a que [$1-e_1=0$] lo cual es contradictorio.

Un uso inmediato de la inversa de vectores es la obtención de la parte proporcional y la parte ortogonal de un vector respecto a otro.[$$b_\parallel=a^{-1} ab_\parallel=a^{-1} (ab+ba)/2$$][$$b_\bot=a^{-1} ab_\bot=a^{-1} (ab-ba)/2$$]Podemos seguir potenciando vectores [$a^k=aa^(k-1)$]. Sin embargo, nos encontramos con una propiedad muy interesante cuando potenciamos cualquier elemento de la base de [$\Lambda(V)$].
[$E=$][$1$][$e_1$][$e_2$][$e_3$][$e_1 e_2$][$e_1 e_3$][$e_2 e_3$][$e_1 e_2 e_3$]
[$E^0=$][$1$][$1$][$1$][$1$][$1$][$1$][$1$][$1$]
[$E^1=$][$1$][$e_1$][$e_2$][$e_3$][$e_1 e_2$][$e_1 e_3$][$e_2 e_3$][$e_1 e_2 e_3$]
[$E^2=$][$1$][$1$][$1$][$1$][$-1$][$-1$][$-1$][$-1$]
[$E^3=$][$1$][$e_1$][$e_2$][$e_3$][$-e_1 e_2$][$-e_1 e_3$][$-e_2 e_3$][$-e_1 e_2 e_3$]
[$E^4=$][$1$][$1$][$1$][$1$][$1$][$1$][$1$][$1$]
[$E^5=$][$1$][$e_1$][$e_2$][$e_3$][$e_1 e_2$][$e_1 e_3$][$e_2 e_3$][$e_1 e_2 e_3$]
[$⋮$][$⋮$][$⋮$][$⋮$][$⋮$][$⋮$][$⋮$][$⋮$][$⋮$]
Así que, para los elementos de una base ortonormal de [$\Lambda(V)$],[$$E^k=E^{k+4}$$] Y, en concreto, se pueden definir las potencias negativas de estos elementos mediante esta propiedad. Desafortunadamente, aunque existe sin problemas, la expresión general de potenciación es bastante más tediosa y no cumple la propiedad de involución arriba vista. Por ejemplo, [$(1+e_1 )^k=2^k (1+e_1 )$].

Tener un producto geométrico cerrado en [$\Lambda(V)$] permite definir la exponencial en este espacio.[$$\exp(A)=\sum_i{\frac{A^i}{i!}}$$] Es fácil comprobar que [$\exp(a+b)=\exp(a)\exp(b)$]. Además, gracias a la involución de los vectores de la base de [$\Lambda(V)$] llegamos a las siguientes ecuaciones:[$$\exp(\alpha)=\cosh(\alpha )+1 \sinh(\alpha )=e^α$$][$$\exp(\alpha e_i )=\cosh(\alpha )+ e_i \sinh(\alpha )$$][$$\exp(\alpha e_i e_j )=\cos(\alpha )+e_i e_j \sin(\alpha )$$][$$\exp(\alpha I)=\cos(\alpha )+I \sin(\alpha )$$] Esta última expresión no es otra que la fórmula de Euler. Además, la penúltima expresión nos da a entender que los elementos de la forma [$\exp (\alpha e_i e_j )$] tienen algo que ver con las rotaciones.

Como ocurría con la potenciación, la exponenciación de elementos generales de [$\Lambda(V)$] no es inmediata y requiere bastantes cálculos. Sin embargo, para vectores [$$\exp \left(\sum_{i=1}^3{\alpha_i e_i}\right)=\prod_{i=1}^3{[\cosh (\alpha_i )+e_i \sinh (\alpha_i ) ]}$$]

Sorpresas geométricas

Probemos a usar [$\exp(\alpha e_1 e_2 )$] para intentar rotar un vector con la intuición que hemos ganado arriba. En el caso de [$\alpha=\pi/2$] la expresión de arriba nos dará [$$\exp \left(\frac{\pi}{2} e_1 e_2 \right)=e_1 e_2=R$$] Este valor [$R$] no rota de por sí un vector. Sin embargo, la expresión [$–RaR$] sí consigue lo que queríamos, aunque el ángulo de la rotación es el doble (debido a que hay dos [$R$]).

Por ejemplo, en el caso de querer rotar [$e_3$], si estamos rotando en el plano formado por [$e_1 e_2$], el resultado ha de ser invariante. Efectivamente:[$$-Re_3 R=-e_1 e_2 e_3 e_1 e_2=-e_1 e_2 e_1 e_2 e_3=e_3$$] Por otra parte, rotar un vector [$a$] en el plano [$e_1 e_2$] media vuelta debe ponerlo en la posición [$-a$].[$$-RaR=-e_1 e_2 (\alpha_1 e_1+\alpha_2 e_2 ) e_1 e_2=-(\alpha_1 e_1 e_2 e_1 e_1 e_2+\alpha_2 e_1 e_2 e_2 e_1 e_2 )=-a$$] Así que la expresión para rotar un ángulo [$\theta$] un vector [$a$] en el plano formado por los vectores ortonormales [$b$] y [$c$] es [$$ -\exp\left(\frac{\theta}{2}bc\right)a \exp\left(\frac{\theta}{2} bc\right)=-RaR$$]
Simple.

¿Qué ocurre si en vez de usar un valor [$R=bc$] usamos un [$R=b$]? Lo que obtenemos entonces es una reflexión. Es sencillo observar que si [$R=e_1$] entonces [$$R e_1 R=e_1,R e_2 R=-e_2,R e_3 R=-e_3$$][$$RaR=R(\alpha_1 e_1+\alpha_2 e_2+\alpha_3 e_3 )R=\alpha_1 e_1-\alpha_2 e_2-\alpha_3 e_3$$] Esto último no es más que la simetría en el eje de la dirección [$e_1$]. Se puede generalizar a cualquier vector unitario y, además, coincide con la idea intuitiva de que una rotación es equivalente a dos reflexiones.

Pensamientos finales

Quedan muchas más cosas por descubrir en el álgebra y el cálculo geométrico. Por ejemplo, se unifican los operadores diferenciales de gradiente, divergencia y rotacional a un único operador diferencial que los engloba. Sin embargo, este artículo ya está quedando largo y sólo me queda dirigir al lector a la Wikipedia y los libros que allí se referencian.

lunes, 13 de febrero de 2012

miniSL parte 18 - Funciones auxiliares del escaneador léxico

En la entrada anterior usamos SkipWhitespace() que automágicamente se saltaba todos los caracteres en blanco hasta el siguiente carácter. La función no es realmente complicada y se limita a ir observando con peek() el siguiente carácter para tirarlo con get() si es un espacio en blanco.

La única variación va a ser el reconocimiento de comentarios. Los comentarios empiezan con un # y terminan con el final de línea.

void Script::SkipWhitespace(ISTREAM& i)
{
 while(i.good() && unsigned(i.peek())<256 && (iswspace(i.peek()) || i.peek()=='#'))
 {
  if(i.peek()=='#')
  {
   i.get();
   while(i.good() && i.peek()!='\n')
    i.get();
  }
  else
   i.get();
 }
}

Es importante dejar claro aquí que no vamos a soportar UNICODE (ni siquiera el famoso "pila de caca") y cualquier carácter por encima del 255 será considerado extraño. De ahí la condición en la guarda del bucle.

Trabajar con iostreams es un poco pesadilla porque no usa excepciones para cosas que deberían ser excepciones (de hecho, los gurús del C++ se quejan a menudo de esta parte de la biblioteca estándar). Poco más podemos hacer para remediarlo que ir comprobando con i.good() cada vez.

Otra función que se usará de vez en cuando es la que espera consumir un token raw (un carácter) en concreto. Es muy simple.

void Script::ConsumeRawToken(ISTREAM& i, ISTREAM::int_type c, wchar_t const* msg)
{
 TOKEN t=ReadToken(i);
 if(t.type!=T_RAW || t.raw!=c)
  throw msg;
}

Si el carácter no es el esperado, lanzamos una excepción con el mensaje. Como hablamos de caracteres, el token ha de ser T_RAW.

Con esto acabamos las funciones auxiliares y pasamos a la lectura de cadenas en la siguiente entrada.

jueves, 19 de enero de 2012

Pasos en la profesión

Una idea que quiero concretar antes de que se me olvide.
  1. Sueños: En este momento sólo se tienen ideas de la profesión. Suele ocurrir cuando se es aún un niño.
  2. Realización: Las ideas se concretan en obras. Si se quería ser alfarero, se realizan las primeras vasijas. Si se quería ser programador, se terminan los primeros programas.
  3. Estudio: Siempre me ha llamado la atención que, sea cual sea la disciplina que se estudie, siempre hay una grandísima cantidad de temas de estudio. Ni que decir tiene que para progresar en la profesión, hay que estudiar. Aunque tampoco demasiado porque si no, no pasamos al siguiente punto.
  4. Práctica: Generalmente el resultado de la realización de ideas no es lo que se tenía en mente. Hay que practicar para que el abismo entre imaginación y realidad se haga más somero. Este es el paso más difícil desde mi punto de vista, ya que se debe trabajar mucho pero no hay realimentación de resultados.
  5. Mercado: Llega un momento en el que las obras realizadas se pueden mostrar al público e, incluso, algunas, vender. Estamos tocando el mercado. Empieza a haber resultados.
  6. Profesión: Es cuando se pueden obtener ingresos regulares de lo que se crea y esos ingresos son suficientes para vivir.
  7. Reconocimiento: El trabajo que se realiza es reconocido entre los que se dedican a lo mismo.
  8. Éxito: Simplemente, los ingresos son muy superiores a lo que se necesita para vivir. Si se dejase de trabajar, seguiríamos teniendo ingresos por regalías o licencias.
  9. Fama: El trabajo que se realiza es reconocido entre los que no se dedican a lo mismo.
  10. Inmortalidad: El trabajo que se realiza sigue siendo reconocido generaciones después.
Es interesante dar un factor de filtrado a cada paso. Así, si de cada 10 que tienen ideas, 1 las realiza; el factor es de 10. De realización a práctica el factor puede ser de 5, pero de práctica a mercado creo que es 20 o más. De mercado a profesión puede ser de 5 o así. Es decir que, a bote pronto, una de cada 5000 personas puede vivir de sus sueños.

Es una estimación algo burda, pero tomándolo como un problema de Fermi el resultado nos puede dar a ver la dificultad de la tarea.

jueves, 1 de diciembre de 2011

miniSL parte 17 - El analizador léxico

Hasta aquí hemos visto la semántica de nuestro lenguaje script mínimo. A partir de ahora, empezamos con el reconocedor sintáctico. Debido a que nuestro lenguaje tiene una sintaxis no trivial, nos ocupará bastante tiempo describirlo. (Una sintaxis trivial es la del LISP)

Generalmente los reconocedores sintácticos se separan en dos partes (por ejemplo, el clásico LEX&YACC). La primera llamada analizador léxico (scanner o lexer) reconoce varios caracteres del fichero de entrada (por ejemplo 3, 5 y 6) y nos devuelve un elemento significativo llamado token (el número 356). La segunda parte es el analizador sintáctico propiamente dicho (llamado parser) que acepta los tokens y devuelve un AST. Veremos esta segunda parte más adelante y nos centramos en la generación de tokens.

En miniSL hay varios tipos de token que se listan en la enumeración TOKEN_TYPE.
enum TOKEN_TYPE { T_NONE, T_RAW, T_LITERAL, T_SYMBOL, T_NAME };
El tipo T_NONE indica que no se han reconocido los caracteres. Usamos T_RAW cuando el token es un único carácter (por ejemplo, una llave { o una coma , ). Es importante saber aquí que el final de fichero, un carácter llamado EOF, también se guarda en un token de tipo T_RAW. El tipo de token T_LITERAL indica o bien un número, o bien una cadena, en general una celda literal. Guardaremos el valor que corresponda en una celda del tipo adecuado. El token de tipo T_SYMBOL es un identificador formado por símbolos tales como +, <= o !=. Finalmente, T_NAME se usa para identificadores alfanuméricos como x, map o iterator. Tanto los tokens de tipo T_SYMBOL como los T_NAME guardan el identificador en una celda de tipo STRING_LIT.

La información del tipo de token y la información relativa al token se guardan en la estructura TOKEN que es la siguiente.
struct TOKEN
 {
  TOKEN() : type(T_NONE) {}
  TOKEN(TOKEN_TYPE t, CELL* d) : type(t), data(d) {}
  TOKEN(ISTREAM::int_type r) : type(T_RAW), raw(r) {}

  TOKEN_TYPE type;
  union
  {
   CELL* data;
   ISTREAM::int_type raw;
  };
 };
Los constructores fuerzan que cuando el TOKEN sea de tipo T_RAW, la información relacionada es un único carácter guardado en la variable miembro raw. En los otros tipos lo guardaremos en la celda apuntada por la variable miembro data. Esta estructura no es muy segura porque podríamos colar un T_RAW en el constructor que toma una celda. Vigilaremos que eso no ocurra.

Ahora queda leer los caracteres y generar el token correspondiente. De eso se encarga la función ReadToken(). La función ReadToken() devuelve el siguiente token leído y lo consume. De esta forma, cada vez que llamemos a ReadToken() avanzaremos por el fichero hasta obtener el carácter de fin de fichero EOF. Sin embargo, muchas veces no querremos avanzar por el fichero. Querremos únicamente saber cuál es el siguiente token, sin consumirlo. De esto se encarga la función PeekToken().

Nota: “Peek” en inglés significa “ojear” en el sentido de mirar a hurtadillas.

Script::TOKEN Script::PeekToken(ISTREAM& i)
{
 if(m_Ahead.type!=T_NONE)
  return m_Ahead;

 m_Ahead=ReadToken(i);
 return m_Ahead;
}
El funcionamiento de PeekToken() es muy simple porque se basa en ReadToken(). Primero, comprueba si ya hemos leído el siguiente token que se ha guardado en m_Ahead. Esta no es más que una variable miembro en la clase Script (para los que no recuerden la clase Script, ver la parte séptima).
TOKEN m_Ahead;
Si m_Ahead guarda ya el token leído, lo devuelve. Si m_Ahead no contiene el token leído, lo lee con ReadToken() y lo guarda en m_Ahead para siguientes PeekToken()s. De esta manera, llamar a PeekToken() varias veces, sólo llama a ReadToken() la primera vez y usa m_Ahead el resto.

¿Por qué es importante saber cómo funciona PeekToken() antes de saber cómo funciona ReadToken()? Muy sencillo. Si m_Ahead contiene un token y llamamos a ReadToken(), hay que vaciar m_Ahead. De esta forma se consume.
Script::TOKEN Script::ReadToken(ISTREAM& i)
{
 if(m_Ahead.type!=T_NONE)
 {
  TOKEN t=m_Ahead;
  m_Ahead.type=T_NONE;
  return t;
 }
A partir de este momento empieza, realmente la lectura de caracteres dentro de ReadToken(). La primera acción es saltarse el espacio en blanco. Nuestra sintaxis va a ignorar los espacios en blanco.
SkipWhitespace(i);
Luego, vamos a ojear el siguiente carácter. Podréis comprobar que no he inventado nada nuevo al llamar PeekToken() a mi función ya que la biblioteca estándar de C++ llama a esta función peek().
ISTREAM::int_type c=i.peek();
Según sea ese carácter, decidiremos leer un token de un tipo o de otro. Si es una doble comilla, será una cadena.
if(c=='"')     return TOKEN(T_LITERAL, &ReadString(i, false));
Si es un símbolo ASCII extendido, lo examinaremos. Para eso usamos la comparación c<256.
if(c<256)
 {
  if(isalpha(c) || c=='_') return ReadName(i);
  if(isdigit(c))    return TOKEN(T_LITERAL, &ReadNumber(i));
  if(CELL::IsSymbol(c))  return TOKEN(T_SYMBOL, &ReadSymbol(i));
Hasta aquí todo sencillo. Si es un carácter alfanumérico, lo leemos como un nombre. Si es un dígito, como un número. Si es un símbolo, como símbolo.

Pero ahora vamos a añadir una puerta trasera que ya ha sido muy útil. De hecho, nos ha permitido imprimir de manera uniforme los identificadores. Básicamente consiste en poner una arroba delante de una cadena. Entonces, en vez de cadena tomamos ese token como un identificador. Será lo mismo escribir mi_variable que @”mi_variable”. También añadiremos la sintaxis de arroba seguido de símbolo con la misma idea. Será lo mismo escribir 3+5 que @+(3,5).
if(c=='@') //Symbol or string as name
  {
   i.get();
   if(i.peek()=='"')    return TOKEN(T_NAME, &ReadString(i, true));
   if(CELL::IsSymbol(i.peek())) return TOKEN(T_NAME, &ReadSymbol(i));
   throw L"Expecting a symbol or a string after @";
  }
 }
Si el carácter no es ASCII extendido, será probablemente el carácter EOF así que lo guardamos como un token T_RAW. Además, si ninguna de las condiciones anteriores se dio, será un carácter ASCII extendido no usado por otro tipo de token. Deberá ser T_RAW también.
//Raw one-character token
 return TOKEN(i.get());
} 
Nota: Esto no funciona con UTF-8 que sería lo suyo. Introduciríamos una complejidad que sobrepasa los objetivos de este pequeño lenguaje de script.

Y ya hemos leído el token. Ahora queda examinar todas esas funciones de ayuda que hemos utilizado. Empezaremos con SkipWhitespace() en la siguiente parte y seguiremos con ReadString() y demás más adelante.

miércoles, 16 de noviembre de 2011

Historia de la gestión de símbolos

PARTE I: COMPILADOR Y VINCULADOR

Hoy en día damos por hecho muchas cosas que han costado muchos años descubrir, usar y pulir. Una de ellas es la gestión de los símbolos. Allá por los años cincuenta, si alguien quería hacer un programa debía escribir el código fuente y, como en estos años los compiladores eran muy simples, directamente te generaban el código ejecutable.


Usualmente el código fuente estaba en tarjetas perforadas y el ejecutable se quedaba en memoria, donde se ejecutaba.

Luego, conforme pasaban los años, los programas se iban haciendo más y más grandes. Recompilar todo cada vez era tedioso y para reutilizar un trozo de código hay que volverlo a escribir. La solución es partir los programas. El problema es que este compilador tan simple no me permite partir los programas.


La solución fue usar dos pasos a la hora de compilar. El primer paso es la compilación a un código intermedio que se denomina código objeto. El código objeto es como el código ejecutable, pero tiene marcas de dónde está cada objeto del código (lo que técnicamente se denomina un símbolo exportado). De ahí el nombre "código objeto". Luego, un segundo paso es el vinculador (o enlazador) que lee esas marcas del código objeto y como si fuera un puzle va componiendo el programa ejecutable final y completo.


Esto causa un problema porque... ¿cómo sabe el código fuente de la parte 1 lo que hay en el código fuente de la parte 2 si puede que incluso hayan sido programados por gente completamente distinta?


PARTE II: SÍMBOLOS Y SÍMBOLOS EXTERNOS

A finales de los sesenta apareción un lenguaje que aún hoy es de los más usados. El lenguaje C. Una de las virtudes del C es que soluciona el problema antes mencionado. La solución que se usa en C es permitir usar objetos sin definirlos. Esto se realiza mediante lo que se denomina la declaración del símbolo. La declaración da toda la información necesaria para usar el símbolo, pero no dice qué es. No lo define.

Lo más usual es declararlos y definirlos que es lo que pongo aquí como ejemplo:

int a=0;

double hipotenusa(double x, double y)
{
   return sqrt(x*x+y*y);
}

El código objeto generado por este código fuente de ejemplo tiene lo que técnicamente se denomina dos símbolos: El símbolo "a" y el símbolo "hipotenusa". Ambos están definidos, es decir, que hay un trozo de ese código objeto que se asigna al símbolo "a" y otro trozo de ese código objeto que se asigna al símbolo "hipotenusa". El trozo del símbolo "a" está a cero. El trozo del símbolo "hipotenusa" tiene el código ejecutable de la función hipotenusa.

El contenido del código objeto sería algo como

SímboloObjeto asignado
a0
hipotenusa
{return sqrt(x*x+y*y);}

Para declarar sin definir los símbolos en C se usa lo siguiente:

extern int a;

double hipotenusa(double x, double y);

El código objeto generado por este código fuente de ejemplo tiene dos símbolos: "a" e "hipotenusa". Ninguno está definido, es decir, que este código objeto sólo tiene una lista de los símbolos sin asignarle ningún objeto. Los símbolos sin objeto asignado tienen que obtenerse del exterior cuando el vinculador recomponga el puzle. Son los llamados símbolos externos, porque se definen en algún otro lugar externo al código fuente donde los declaramos.

SímboloObjeto asignado
aexterno
hipotenusaexterno


PARTE III: USANDO SÍMBOLOS EXTERNOS

Ahora ya sabemos cómo declarar y definir una función en código fuente y cómo usarla desde otro código fuente. Bastará declararla sin definirla.

programa_parte1.c

/* Declaramos y definimos */
void mifuncion(void)
{
   puts("Esta función está programa_parte1.c");
}

programa_parte2.c

/* Declaramos pero no definimos (prototipo) */
void mifuncion(void);

void main(void)
{
   puts("Esta función está en programa_parte2.c");
   mifuncion(); /* Se usa mifuncion() con haberla sólo declarado */
}


Primero compilamos


Según el compilador la extensión del código objeto será .o   .obj   .coff, etc. Los códigos objetos generados serán algo así como muestran las siguientes tablas.

programa_parte1.obj

SímboloObjeto asignado
mifuncion
{puts("Esta función está programa_parte1.c");}


programa_parte2.obj

SímboloObjeto asignado
mifuncionexterno
main
{ puts("Esta función está en programa_parte2.c"); mifuncion(); }

Ahora pasamos el vinculador o enlazador.


El vinculador es lo suficientemente inteligente como para ver que el símbolo "mifuncion" está definido en la parte 1 y es un símbolo externo en la parte 2 donde se usa. Entonces, compone ambas partes haciendo que cuando la función main() llame a mifuncion() lo haga correctamente. Finalmente el símbolo "main" es especial y lo usa como punto de entrada del ejecutable.


PARTE IV: LOS FICHEROS DE INCLUSIÓN

Ahora sólo queda un detalle menor. ¿Vamos a tener que repetir

void mifuncion(void);

cada vez que queramos usar la función mifuncion()? ¿Qué pasará si lo que tengo en el código objeto no es una función sino una biblioteca de cincuenta, cien o mil funciones? ¿Voy a tener que escribir todas las que quiera usar una y otra vez en cada fichero fuente que escriba?

La solución es poner todas las declaraciones en un fichero de cabecera (.h del inglés header). E incluir el fichero con tooodas las definiciones. Esto nos ahorra el escribir cada vez todas las definiciones.


Este uso de los ficheros de inclusión es común en lenguajes como el C y el C++.


PARTE V: LOS MÓDULOS

Realmente, usar un fichero de inclusión es una tontería: ¡ya tenemos los símbolos en el código objeto! ¿No podríamos decir en el código fuente "toma los símbolos de este código objeto"? La respuesta es sí. Existen muchos lenguajes que lo hacen. Entre ellos Java y ActionScript3. A esta acción de extraer símbolos de un código objeto y usarlos en otro código fuente se denomina "importar símbolos". Al código objeto que exporta los símbolos de esta manera se le denomina módulo o paquete.


Existen un par de salvedades a este sistema. En primer lugar, el orden de compilación es importante. Debemos compilar los códigos fuentes de manera que cuando usemos un símbolo, el código objeto que lo contiene haya sido generado previamente. Esto significa que no podemos tener ciclos en el uso de referencia aunque algunos sistemas mezclan el compilador y el vinculador en un único paso para permitir estos ciclos. En segundo lugar, es importante cómo llamamos a los ficheros de código objeto y dónde los ubicamos en el sistema de fichero.

Esto último hace que sea necesaria una jerarquía en los nombres de los módulos, usualmente separados por puntos. Así, un símbolo del sistema de módulos de Java se escribe así "java.lang.NullPointerException" y significa algo como en el directorio "java", en el subdirectorio "lang", el fichero "NullPointerException". Este tipo de nombre de símbolos compuestos de varias partes se llaman nombres completamente cualificados.

Es usual en estos sistemas de módulos que cada fichero sólo pueda definir un símbolo. Aquél que coincide con el nombre del fichero. Hay aquí una rigidez. ¿Podríamos separar los nombres de los ficheros de los nombres de los símbolos?

PARTE VI: LOS ESPACIOS DE NOMBRE

Cuando usamos un nombre cualificado como  "java.lang.NullPointerException" hemos pensado que "java" es un directorio, pero desde el punto de vista del programador no es más que un nombre que contiene otros nombres, entre ellos "lang". Asimismo "lang" contiene "NullPointerException". Por esta razón, es común llamar a "java" y a "java.lang" un espacio de nombres (namespace en inglés). En el caso de Java (y de AS3) los espacios de nombres están relacionados con los directorios.


Sin embargo, en C# y C++ no es así y pueden definirse espacios de nombres en el código fuente. Eso significa que los códigos objetos exportan los símbolos con sus nombres cualificados y el vinculador tiene que ser un poco más inteligente.

Modifiquemos el programa anterior para incluir un espacio de nombre llamado "miespacio" usando C++ y añadiré otra "mifuncion" en "otroespacio". El C++ es exótico porque usa :: para separar los nombres cualificados en vez del punto.

programa_parte1.cpp
/* Declaramos y definimos */
namespace miespacio
{
  void mifuncion(void)
  {
     puts("Esta función está programa_parte1.c y en miespacio");
  }
}

namespace otroespacio
{
  void mifuncion(void)
  {
     puts("Esta función está programa_parte1.c pero en otroespacio");
  }
}

programa_parte2.c
/* Declaramos pero no definimos (prototipo) dentro de miespacio*/
namespace miespacio { void mifuncion(void); }

void main(void)
{
   puts("Esta función está en programa_parte2.c");
   miespacio::mifuncion(); /* Se usa miespacio::mifuncion() pero no otroespacio::mifuncion() */
}

Si ahora miramos los códigos objeto encontramos los siguientes símbolos.

programa_parte1.obj    
SímboloObjeto asignado
miespacio::mifuncion
{puts("Esta función está programa_parte1.c y en miespacio");}
otroespacio::mifuncion
{puts("Esta función está programa_parte1.c pero en otroespacio");}

programa_parte2.obj
SímboloObjeto asignado
miespacio::mifuncionexterno
main
{ puts("Esta función está en programa_parte2.c"); miespacio::mifuncion(); }


De esta manera no hay relación entre la ubicación del módulo en el sistema de ficheros y los espacios de nombre que contiene. Esto es importante porque ahora podemos mover el módulo de lugar dentro del disco y no hay que modificar el código fuente.