El siguiente listado muestra otra de las características "experimentales" de Freya: los filtros de iteración.
for i in 2..Max div 2 : not Result[i] do
begin
// ... etcétera...
end;
Freya
Se trata de un pequeño fragmento del algoritmo de la
Criba de Eratóstenes, implementado
en Freya, y muestra el uso de iteración mediante rangos y filtros. El bucle recorre valores enteros entre dos límites, pero sólo entrega aquellos números que no han sido marcados como primos todavía. El compilador traduce esta instrucción
for en un bucle
for numérico "clásico" con una instrucción
if/
then anidada. ¿Para qué tomarse entonces la molestia?
- El minimalismo en lenguajes no es necesariamente bueno
Esa es una curiosa superstición, que se desenmascara mirando lo que ocurre con Java. En este lenguaje, el repertorio de constructores de tipos es muy limitados: nada de delegados, nada de enumerativos, tipos de valor los justos... ¿Traspaso de parámetros por referencia? ¡Anatema, pecador! Copie el valor a modificar en la primera entrada de un array de un solo elemento, y pase el array como parámetro. ¿Necesito seguir?
- La combinación de rango más filtro aumenta mucho la expresividad
Mi teoría es que, en este caso, una simple ojeada a la cabecera del bucle deja las intenciones muy claras. Si no me cree, piense que ha escrito un for con un if anidado, y que la instrucción condicional tiene un bloque de instrucciones relativamente largo. ¿Cuánto tiempo tardaría en averiguar si existe una cláusula else o si se trata de un simple if/then? Espero que coincida conmigo en que se trata de dos patrones algorítmicos muy diferentes.
Confieso, no obstante, que llegue a lo creo un buen diseño gracias a una confusión. Freya se está diseñando, desde el primer momento, mediante un método que podríamos llamar mutación, erosión y selección, siguiendo un patrón similar al que ocurre con los lenguajes humanos. La mutación puede ocurrir deliberadamente o por error. Por ejemplo, en los primeros días, cuando Freya se parecía muchísimo más a Delphi, escribí por error una declaración de variable "global" dentro de una sección de implementación. Pero me di cuenta enseguida de que podía reinterpretar la declaración como una variable de instancia privada. Este un ejemplo de mutación. Un buen ejemplo de erosión es la omisión del nombre de la clase en el constructor. Al principio, el constructor llevaba el mismo nombre de la clase, pero un buen día olvidé escribir el nombre... y me di cuenta que era innecesario. Naturalmente, la selección es el proceso posterior a estos errores: es cuando me digo "y vio Ian que era bueno".
Miranda
¿Cuál fue mi error inicial, en este caso? Freya ya tenía rangos, que permitían tres usos: el uso trivial en instrucciones
case, como operandos en el lado derecho del operador
in y como iteradores en la instrucción
for... sin filtros en aquel momento. Cuando me di cuenta de la frecuencia de aparición de iteración más selección en los ejemplos, pensé en introducir algo parecido a las
expresiones de Zermelo-Frankel. Este ejemplo pertenece a
Miranda:
[ n | n <- [1..5] ; n mod 2 = 0 ]
Una expresión ZF, o list comprehension, en la jerga funcional, permite directamente las tres operaciones más importantes soportadas por LINQ: iteración, selección y proyección. ¿Por qué no sustituir ese desagradable dialecto llamado SQL (¡sí, SQL como lenguaje es malísimo!) por un formalismo más elegante? Tenga presente que en mi ejemplo, el filtro se ha aplicado a un rango, pero en realidad, Freya permite usarlo con cualquier iterador...
En realidad, se trata de dos errores. El primero, es que la combinación de iterador más filtro no es, hablando con propiedad, una expresión de Zermelo-Frankel completa, pues necesita la variable de iteración para tener sentido. La primera consecuencia es que la idea no puede trasladarse automáticamente al operador in. Para verlo, sólo intente escribir una expresión in sobre un iterador filtrado. Es cierto que estas expresiones de pertenencia no ganan mucho cuando el iterador es un rango numérico, pero tenga presente que Freya permite usar el operador in con cualquier tipo que publique un método Contains (sí, hay que ampliarlo todavía para que maneje el tipo IEnumerable a secas).
LINQ
No se trata, sin embargo, de un error grave: a pesar de no poder aplicarse directamente a las pruebas de pertenencia, el recurso sigue siendo útil para los bucles
for, como ya he explicado, por lo que mantuve la idea. El segundo error es más interesante: ¿pueden realmente las expresiones ZF reemplazar a LINQ? Con las debidas extensiones... sí. Pero estamos perdiendo de vista algo muy importante. Mi idea, realmente, era poder compilar
eficientemente las expresiones de este tipo. Ahora mismo, C# 3.0 permite escribir código como el siguiente:
List<int> lista = { 1, 2, 3, 4, 5 };
if (lista.Exists(x => x % 2 == 0))
{
// ... etcétera...
}
No se trata de LINQ directamente, por cuestiones sintácticas, pero sí es la base de la implementación de LINQ: la prueba de existencia se convierte en una llamada a un método que recibe un delegado (un puntero a método, al fin y al cabo). ¿Eficiencia de la llamada? Asquerosa. Sí, es muy expresivo, pero sería mucho más rentable expandir el bucle de búsqueda en línea... si éste no consumiese tanto espacio y no ocultase nuestras intenciones.
Imagine ahora lo que ocurrirá cuando LINQ esté disponible: en principio, le permitirá acrobacias más complicadas con la nueva sintaxis... al precio de convertir su aplicación en una lenta babosa caracolera. ¿Significa esto que LINQ es un error? ¡De ningún modo! Pero el motivo por el que LINQ tiene sentido no está siendo debidamente explicado por los evangelistas entusiastas de la idea que pululan por foros y bitácoras. Y no me di cuenta del problema hasta haber experimentado con la característica de Freya que acabo de explicar.
Code is data
El verdadero corazón y sentido de LINQ no son esas amaneradas imitaciones de la sintaxis de SQL... sino los
árboles de expresiones. Seamos sinceros: si LINQ sólo se pudiese usar con estructuras en memoria como las listas, sería una porquería del tamaño de la suma de todos los libros impresos sobre Java. Sería una porquería porque no habríamos logrado nada compilando estas construcciones como llamadas a métodos de la biblioteca de clases. Excepto ineficiencia, claro...
¿Por qué hay que llamar, entonces, a métodos de la biblioteca de clases? Pues porque, en algunos casos, como deja entrever la publicidad sobre LINQ, estos métodos, en vez de recibir delegados asociados a métodos anónimos, exigirán que el compilador les pase esos misteriosos árboles de expresiones. ¿A qué tipo de datos pertenece el parámetro del método en la siguiente llamada?
sequence.Where(x => x % 2 == 0)
En la mayoría de los casos, se tratará de un tipo delegado, pero podría tratarse de un árbol de expresiones. En ese segundo caso, sería el propio compilador de C# (como muestra la beta de Orcas) quien debería crear una estructura en memoria que reflejase la estructura de la expresión lambda para pasarla como parámetro. Es un comportamiento importantísimo, pero muy poco documentado.
¿Para qué puede querer un método recibir un árbol de expresiones, en vez de un puntero a método? Motivos sobran. Por ejemplo, el método puede transformar el árbol en una cadena que represente una instrucción SQL para pasarla a un servidor SQL. O el método puede formar parte de la capa de comunicación con un servidor remoto, y enviar una versión serializada del árbol para que sea evaluado al otro lado de la red. Estos son los casos reales de uso de LINQ, no esas tonterías de manipulación de listas en memoria que muestran casi todos los ejemplos.
¿Se da cuenta que LINQ, hasta cierto punto, nos retrotrae a los tiempos en que estaba de moda considerar el código como datos? De hecho, ¡podemos convertir fácilmente un árbol de expresiones en código nativo ejecutable! Si usted guarda fórmulas para los gastos de envío en una base de datos, seguirá necesitando un pequeño analizador sintáctico para convertirlas en árboles de expresiones. Pero ya podrá delegar en el sistema la conversión del árbol en código. ¿No es estupendo?
Resumiendo: LINQ es una buena idea, pero está siendo mal explicada. El riesgo que se corre es que aparezcan aplicaciones que utilicen LINQ de forma incorrecta... como, efectivamente, sugieren la mayoría de los ejemplos disponibles, incluso de la propia Microsoft. En segundo lugar, una vez que se comprende este problema, se comprende también que existe una puerta abierta en el diseño de lenguajes para .NET. Puede que sea beneficioso aumentar la potencia expresiva de las operaciones sobre listas mediante una sintaxis especial, paralela a la de LINQ. O puede que lo indicado sea la optimización de las llamadas a determinados métodos de determinadas clases, ya sea por medio del compilador o por medio de alguna utilidad aplicada al ensamblado compilado... pues dudo que el JIT llegue a estos extremos.
Etiquetas: Freya, ideas, LINQ, sintaxis