jueves, agosto 23, 2007

LINQ, Freya, Miranda

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

FreyaSe 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?
  1. 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?
  1. 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

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

The Missing LINQNo 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

OuroborosEl 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: , , ,

10 Comments:

Blogger PabloNetrix said...

Plas, plas, plas.

Esto no se merece un 'chapeau', se merece una reverencia digna de la corte de Luis XVI.

Este 'post' lo pondré de 'backtrack' en más de un foro de los que visito, donde la simple mención de la palabra 'LINQ' provoca gemidos de placer... ;)

viernes, agosto 24, 2007 12:35:00 p. m.  
Blogger Ian Marteens said...

Je, je, me van a odiar...

viernes, agosto 24, 2007 1:28:00 p. m.  
Blogger Unknown said...

uuuuuuuffffffff !!!!!!!

viernes, agosto 24, 2007 3:50:00 p. m.  
Blogger Alfredo Novoa said...

for i in 2..Max div 2 : not Result[i] do
begin
// ... etcétera...
end;


El problema de esto es que apenas ahorras escritura de código.

for i in 2..Max div 2 if not Result[i]
begin
// ... etcétera...
end;

Esto tiene un caracter menos y yo creo que hasta queda más claro.

Otra cosa sería meterse realmente en un lenguaje con extenstiones orientadas a conjuntos de verdad, pero eso por supuesto ya está inventado.

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.

Mi primera reacción a esto es que esos no son ni mucho menos los operadores más importantes de un lenguaje de consultas, sino que es un subconjunto con una expresividad extremadamente limitada. Pero pensandolo mejor si que son especialmente importantes para LINQ por que LINQ se va a usar casi siempre con un servidor SQL y si usamos vistas para todas las consultas mínimamente complejas entonces si que nos podríamos apañar solo con la selección y la iteración. Aunque por supuesto la iteración habría que automatizarla todo lo posible por que es una operación extremadamente primitiva.

¿Por qué no sustituir ese desagradable dialecto llamado SQL (¡sí, SQL como lenguaje es malísimo!) por un formalismo más elegante?

Si que es malísimo, pero muy potente y expresivo. Para hacer algo mejor como mínimo tenemos que mantener la potencia expresiva, y para eso se necesitan un montón de operadores además de selección y proyección. Por supuesto este tema está perfectamente estudiado y analizado matemáticamente.

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.


Esto es problema del compilador, de las prisas de una primera versión, y de que le hayan encargado ese trabajo a gente con poca formación en teoría de bases de datos. Por supuesto que esa expresión se puede compilar eficientemente. No es más que una expresión constante.

El verdadero corazón y sentido de LINQ no son esas amaneradas imitaciones de la sintaxis de SQL...

Discrepo, para mi está claro que el sentido fundamental de LINQ es acabar con el código SQL escrito entre comillas que no se puede verificar en tiempo de compilación. Es fundamentalmente una vuelta al viejo SQL empotrado de C que tan buenos resultados sigue dando en comparación a toda esa tontería del ORM anterior a LINQ.

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...

Podemos ganar mucha velocidad de desarrollo, y una buena implementación (que no es el caso) podría superar fácilmente al programador medio o con prisas.

pues dudo que el JIT llegue a estos extremo

No lo dudes, dalo por seguro :)

domingo, agosto 26, 2007 1:15:00 p. m.  
Blogger Ian Marteens said...

Esto tiene un caracter menos y yo creo que hasta queda más claro.

:) pero utiliza la contracción que no gusta a nadie...

por que LINQ se va a usar casi siempre con un servidor SQL

Are you sure? Conozco un lenguaje en el que han introducido un tipo sequence of X... que es sinónimo de IEnumerable[X]. ¿Crees que lo tienen claro? La mayoría de los instructores que revolotean alrededor de Microsoft (al menos, los que escriben en castellano) recomiendan el uso de algún tipo de clases de persistencia. ¿Qué crees que harán cuando tengan LINQ en sus zarpas?

De todos modos, la notación ZF es muy limitada expresivamente, comparada con el álgebra relacional. Es más, las list comprehensions tienen un defecto grave: es difícil distinguir a simple vista entre el generador y los filtros.

Mi experimento con la ZF iba más bien encaminado a la composición declarativa de iteradores. Lo he mencionado para explicar my "insight" sobre LINQ.

Esto es problema del compilador, de las prisas de una primera versión, y de que le hayan encargado ese trabajo a gente con poca formación en teoría de bases de datos. Por supuesto que esa expresión se puede compilar eficientemente. No es más que una expresión constante.

No, no es un problema del compilador. El problema es que la lista puede pasarse como uno de sus tipos bases. La llamada al método es indispensable.

Discrepo, para mi está claro que el sentido fundamental de LINQ es acabar con el código SQL escrito entre comillas que no se puede verificar en tiempo de compilación.

¿Cuánto código de ese tipo tienes en tus aplicaciones? Aunque mi teoría es que hay cosas "inencapsulables", ésta no es de ese tipo, IMHO. Yo, más que código SQL, tengo fragmentos de plantillas de código SQL, casi siempre en un único módulo.

Es fundamentalmente una vuelta al viejo SQL empotrado de C que tan buenos resultados sigue dando

:) Como he dicho, prefiero el Modular SQL (mi tesis de diploma, por cierto). En un "módulo SQL" se declaran cursores que luego se pueden usar desde otro lenguaje como iteradores.

lunes, agosto 27, 2007 4:52:00 p. m.  
Blogger Ian Marteens said...

De todos modos, si tu premisa es que LINQ sólo se usará con servidores SQL (y con servidores de capa intermedia, que es otro punto muy importante, y lo que justifica la existencia explícita de los árboles de expresiones), estamos diciendo prácticamente lo mismo. Otra cosa es que no creo que la gente se dé cuenta de por dónde van los tiros.

En todo caso, hay un problema gordo: la representación factual (o reificada, o "materializada") se detiene en las expresiones escalares. Hasta donde he visto, no hay una estructura de datos que represente una consulta LINQ completa (excepto dentro de las implementaciones particulares de cada API, fuera del alcance del programador).

lunes, agosto 27, 2007 4:56:00 p. m.  
Blogger Ian Marteens said...

... me voy de cursos mañana. Si no doy señales, es que no había buena conexión en el hotel.

lunes, agosto 27, 2007 6:03:00 p. m.  
Blogger Alfredo Novoa said...


:) pero utiliza la contracción que no gusta a nadie...

Yo no he dicho que no me gustase :)

Are you sure?

Si, para eso lo han hecho.

¿Crees que lo tienen claro? La mayoría de los instructores que revolotean alrededor de Microsoft (al menos, los que escriben en castellano) recomiendan el uso de algún tipo de clases de persistencia. ¿Qué crees que harán cuando tengan LINQ en sus zarpas?

Los que recomienden eso son unos cenutrios y LINQ puede venir bien para que no se hagan tanto esas tonterías. Por muy feo y cojo que les haya quedado LINQ desde luego es muchísimo más recomendable que gestionar los datos en las aplicaciones con código procedimental como recomiendan esos mendrugos.

De todos modos, la notación ZF es muy limitada expresivamente, comparada con el álgebra relacional.

Entonces me quedo con el álgebra relacional :-)

Mi experimento con la ZF iba más bien encaminado a la composición declarativa de iteradores.


Ya, lo que pasa es que los iteradores ya son de por si de tan bajo nivel que no se si vale mucho la pena meterse en esas cosas. Si quieres programar un algoritmo a bajo nivel por razones de eficiencia pues entonces todo a bajo nivel.

¿Cuánto código de ese tipo tienes en tus aplicaciones?

Miles de líneas. Mucho menos que en los tiempos de DBase y Paradox, pero todavía tengo que construir muchas consultas dinámicamente. Ahora intento meter todo lo posible en vistas para que el SQL de las aplicaciones sea lo más sencillo posible.

También hago aplicaciones para Pocket PC y ahí no hay vistas y hay que meter un montonazo de código SQL dentro de las aplicaciones.


Aunque mi teoría es que hay cosas "inencapsulables", ésta no es de ese tipo, IMHO.


Pues yo creo que el SQL es de lo más "inencapsulable" que hay. No tiene mucho sentido encapsular un lenguaje de alto nivel como SQL con otro de comparativamente bajo nivel como C#.

Yo, más que código SQL, tengo fragmentos de plantillas de código SQL, casi siempre en un único módulo.

Yo lo del único módulo lo hacía hace años y no estaba mal para aplicaciones relativamente pequeñas, pero para cosas grandes se hacía monstruoso y quedaba un módulo enorme lleno de código que apenas tenía ninguna relación entre si.

Ahora prefiero acercar el código a donde se usa, y con algo del tipo de LINQ pero mejor hecho pues quedaría mucho mejor :-)

:) Como he dicho, prefiero el Modular SQL (mi tesis de diploma, por cierto).


¿Se puede leer? :)

En un "módulo SQL" se declaran cursores que luego se pueden usar desde otro lenguaje como iteradores.

Pero trabajar con cursores e iteradores es una castaña. Yo solo los utilizaría para volcar los resultados. Pero lo evito casi siempre usando "databindings".

Para trabajar con datos cuanto de más alto nivel y orientado a conjuntos sea el lenguaje pues mucho mejor. Y en eso LINQ es un paso adelante. Muy torpe pero adelante.

De todos modos, si tu premisa es que LINQ sólo se usará con servidores SQL (y con servidores de capa intermedia, que es otro punto muy importante, y lo que justifica la existencia explícita de los árboles de expresiones), estamos diciendo prácticamente lo mismo.

Si, en realidad son cosas parecidas. Yo digo que el proposito fundamental del LINQ es acabar con el SQL entrecomillado y la tontería de las "clases de persistencia", y tu mencionas una cosa necesaria para poder hacer esto.

Por cierto que si nos pudiesemos comunicar con los servidores intermedios usando el pseudo-SQL de LINQ sería una bendición.

Otra cosa es que no creo que la gente se dé cuenta de por dónde van los tiros.

Yo creo que los primeros que no se enteran son los creadores de LINQ. Las cosas mejores de LINQ vienen de SQL y las han metido sin darse mucha cuenta de lo que hacían.

Hace poco leí una entrevista a Meijer en la que soltaba perlas como que quería sustituir el álgebra relacional por mónadas X-D

En todo caso, hay un problema gordo: la representación factual (o reificada, o "materializada") se detiene en las expresiones escalares. Hasta donde he visto, no hay una estructura de datos que represente una consulta LINQ completa (excepto dentro de las implementaciones particulares de cada API, fuera del alcance del programador).


¿Estás seguro?

Pues si que parece una cosa fea.

Acabo de echar un vistazo y siempre se podría solucionar a nivel del compilador.

Tampoco está mal que nos dejen algo de diversión para los demás :-)

lunes, agosto 27, 2007 10:43:00 p. m.  
Blogger Alfredo Novoa said...

No, no es un problema del compilador. El problema es que la lista puede pasarse como uno de sus tipos bases. La llamada al método es indispensable.

No te entiendo. Si yo veo fácilmente como optimizar ese código entonces puedo escribir un compilador que haga esas optimizaciones en mi lugar. Para mi está claro que no hay que llamar al método y que el if se puede simplificar a if true.

lunes, agosto 27, 2007 10:52:00 p. m.  
Blogger Andrés Ortíz said...

Ian por favor saca tú repertorio de buenos ejemplos sobre Linq, antes que...

jueves, agosto 30, 2007 7:33:00 p. m.  

Publicar un comentario

<< Home