jueves, enero 17, 2008

SILLY

No, no estoy llamando "tonto" a nadie: es el nombre que se me ocurrió para el lenguaje de descripción de escenas de XSight RT, y significa, simplemente, Small Instantiation Language... claro, añadiéndole el sufijo de los adverbios ingleses.
El caso es que se trata de un lenguaje orientado a la creación de objetos; de ahí lo de Instantiation. Resulta también que hay un gran números de circunstancias en los que esa "categoría" de minilenguajes resulta útil. ¿Quiere otro ejemplo? Ahí tiene la generación de compiladores. Freya utiliza GOLD: un sistema que recibe una gramática y produce una tabla de análisis sintáctico. Pero es más común el uso de sistemas como Yacc y Bison, que permiten asociar instrucciones en C/C++/C# a la gramática. Hace poco encontré este otro generador, de Wayne Kelly, de la Queensland University of Technology. Incluye el código fuente completo del generador, en C#, y es un código limpio y legible.
Tengo, desde hace un tiempo, la idea de ensayar el uso de un lenguaje tipo SILLY con un generador de compiladores para .NET. Como el análisis sintáctico suele utilizarse para crear, en paralelo, un Arbol de Sintaxis Abstracta (AST), el lenguaje se especializaría en la creación de objetos. Algunas ideas sencillas:
  • Nada de operador new. Cuando un nombre de tipo se usa como función, significa la construcción de una instancia.
  • El lenguaje soportaría inicializadores de objetos... al estilo Freya, claro. Es decir, se podrían inicializar campos y propiedades del objeto creado usando una sintaxis similar a la de los parámetros con nombre.
  • Cada "no terminal" de la gramática tendría un tipo de datos asociado.
  • Cada "regla", o "producción", tendría una expresión asociada, de un tipo derivado del tipo asociado al no terminal.
Veamos un ejemplo sencillo:
<Exp> ::=          : AstExp
<Exp> '+' <Term> : AstBinary($Exp, '+', $Term)
<Term> : $Term
La primera línea advierte que las expresiones van asociadas a nodos de la clase AstExp. En la segunda, cada vez que se detecta una suma, se crea un nodo AstBinary a partir de los nodos de las partes constituyentes. La tercera línea indica que se copie, simplemente, el nodo asociado al término. Este es, naturalmente, el caso más sencillo y frecuente. También es frecuente el uso de listas, por lo que esta variante de SILLY debería soportarlas:
<VarGroup> ::=         : List[AstVar]
<VarGroup> ',' <Var> : $VarGroup + { $Var }
<Var> : { $Var }
Es decir, las llaves se utilizarían para delimitar literales de listas, y el signo + significaría "unión", o concatenación de listas.
Ahora mire un "invento" curioso, asociado a la regla sintáctica correspondiente al operador existencial de Freya:

<Exp> ::= : AstExp
'*' IN <Exp> : if $Exp is AstRange
then AstBin($Exp.Lo, "<=", $Exp.Hi)
else AstExists($Exp)
Para empezar, observe que he mostrado una expresión condicional, no una instrucción condicional. Lo interesante es lo que ocurre en la rama then de la expresión: como esa rama se evalúa cuando $Exp es un AstRange, cambiamos el tipo declarado para $Exp de esa rama para abajo (en el árbol de expresiones); por eso permitimos las referencias a las propiedades Lo y Hi (low y high), definidas para AstRange. Creo que un convenio de este tipo ahorraría mucho trabajo y sería muy útil en lenguajes de propósito general... aunque tengo que pensarlo un poco más, antes de dar el recurso por bueno.
Naturalmente, se permitiría expresiones let/where, y quizás sería necesario algún "aplicador" para actuar sobre elementos de una lista. El "compilador" tendría que encargarse también de algo que podemos clasificar como inyección de código: se supone que, a la vez que se construye el árbol sintáctico, los nodos de éste deben irse asociando a intervalos o rangos de texto dentro del código fuente. A los nodos devueltos en cada reducción, por ejemplo, se les podría asociar automáticamente al rango determinado por el texto reducido. Pero habría casos más complicados:
<Exp> ::=            : AstExp
<Exp> IS NOT <Ref> : AstNeg(AstCast($1, $3))
El nodo AstNeg es el inmediatamente devuelto por la reducción, por lo que es fácil asignarle un rango. En cambio, el nodo AstCast se esconde dentro del nodo principal. Una solución laboriosa sería indicar explícitamente la asignación del rango:
AstNeg(AstCast($1, $3, Range := @1 + @3))
Este es un ejemplo de los inicializadores de objetos ya mencionados. Observe que @1 se refiere al rango asociado al primer nodo de la regla. Otra solución más elegante sería permitir que el compilador dedujese dicha asignación, a partir de los parámetros detectados en la llamada al constructor AstCast.
¿Qué le parece?

Etiquetas: , ,

viernes, enero 11, 2008

Estadísticas y curiosidades

Me ha sorprendido que cierto artículo sobre ADO.NET 2.0, escrito casi al principio del ciclo de vida del producto, cuando aún se llamaba Whidbey, se sigue descargando de mi página el doble de las veces que Intuitive C#, el libro gratuito sobre el lenguaje. No hay nada de malo en ello, excepto que no tenía consciencia del hecho... y que me preocupa la actualidad del contenido del famoso artículo, como podrá imaginar (no he incluido el enlace para no "agravar" el fenómeno). Supongo que tendré que retocar un poco el contenido, para evitar confusiones. También empezaré a fechar los artículos: tengo algunas malas experiencias con citas, por parte de terceros, de artículos del año cataplum, con referencia a tecnología de aquellas fechas.
Por cierto, no se hemos pasado todavía la primera quincena del año, ¡y ya aparecen en mis estadísticas varias consultas sobre el principio de indeterminación de Heisenberg!

Por cierto, si aún sigue usando la versión 2005 de Visual Studio... ¡pásese ya a la 2008! Visual Studio 2008 permite configurar la versión de .NET Framework para la que compila los proyectos. De manera que, si no quiere migrar sus aplicaciones a .NET 3.5 por el motivo que sea, no está obligado a hacerlo: puede seguir usando .NET 2.0, pero con Visual Studio 2008, y postergar el cambio de versión de su aplicación per secula seculorum. ¿La ventaja?, pues que, a pesar de seguir usando esa versión de la plataforma, Visual Studio 2008 le permitirá usar buena parte de las extensiones de C# 3.0.
Desventajas: VS2008 está todavía en inglés. Hay también un punto negro: las operaciones "Buscar todas las referencias" y el cambio de nombre de un identificador tardan más que en VS2005, o esa es la impresión que me he llevado. He leído en algún lugar que, en esta versión, el equipo de VS2008 unificó los bindings: en la jerigonza del entorno de desarrollo, eso de los "bindings" se refiere al uso de partes del compilador y del analizador lexical para asuntos como IntelliSense, la navegación por clases y miembros, etc. Es algo muy normal que, en un entorno de desarrollo como VS2008, se utilicen distintos compiladores y analizadores especializados para estas tareas, con el imaginable coste adicional del mantenimiento de los mismos. Al parecer, han hecho cambios para unificar la base de código... y mi impresión es que algunas operaciones son ahora más lentas. Pero, en general, creo que merece la pena el cambio.

Etiquetas:

miércoles, enero 09, 2008

Clases anidadas dentro de interfaces

C# no lo permite, pero el CLR no pone objeciones:
.class public interface abstract auto ansi MyInt
{
.class abstract auto ansi sealed nested public MyExt
extends [mscorlib]System.Object
{
}
}
El listado anterior muestra una clase estática anidada dentro de un tipo de interfaz. Naturalmente, para poder crear este "engendro" he tenido que usar el API de reflexión, porque ninguno de los lenguajes actuales acepta un tipo anidado dentro de una interfaz.
¿Para qué necesito esto? Resulta que es la forma más elegante de definir métodos de extensión para un tipo de interfaz:
IStack = interface[X]
IsEmpty: Boolean;
Top: X;
method Push(Value: X);
method Pop;
method Clear;
begin
while not
IsEmpty do Pop;
end;
end;
El truco, por supuesto, consistiría en generar una clase estática adicional, con métodos de extensión. El problema es más bien estético: la proliferación de clases con nombres extraños en el espacio de nombres del programador (el mecanismo usado por C# y compañía para "activar" las extensiones es un desastre).
La solución puede ser anidar la clase con las extensiones en el tipo de interfaz. Primera dificultad: ya hemos visto que está prohibido en los compiladores, pero no en el runtime, de Microsoft. Segunda dificultad: resulta que C# no permite tampoco métodos de extensión dentro de una clase anidada. Esto va a complicar la compatibilidad de los métodos generados en Freya con este mecanismo: lo más que se puede hacer es ofrecer un switch de compatibilidad. Si está inactivo, porque la aplicación o el ensamblado van a ser consumidos desde Freya, los métodos de extensión asociados directamente a interfaces se generarán en una clase anidada. En caso contrario, se utilizará el horroroso sistema actualmente empleado por Microsoft.
Ocurre que existen motivos adicionales para investigar en esta dirección: Freya permite definir aserciones en un tipo de interfaz (la última vez que eché un vistazo a Chrome, éste no lo permitía). Si un tipo de interfaz representa la definición de un "contrato", ¿qué hay más natural que permitir reglas que precisen los términos de dicho contrato? Por desgracia, precondiciones y postcondiciones generan código, y de momento, Freya estaba generando ese código en clases auxiliares. Con el nuevo mecanismo, podremos encapsular este código en clases anidadas, y despejar un poco el espacio de diseño.

... y aprovecho para aclarar un poco la relación entre aserciones, interfaces y métodos de extensión. Supongamos un caso muy sencillo de interfaz con una precondición:
IStack = interface[X]
property IsEmpty: Boolean;
method Pop;
requires not IsEmpty;
// ...
end;
Ahora mismo, Freya genera una clase estática con métodos para cada aserción de la interfaz. Como se trata de miembros de una interfaz, necesariamente públicos, no hay problema con los niveles de acceso. Con la nueva idea de implementaciones en interfaces (formalmente, se puede hablar de traits o mixins, aunque no es exactamente lo mismo), lo que haríamos ahora sería equivalente a generar un método "de extensión" en la interfaz:
IStack = interface[X]
property IsEmpty: Boolean;
method Pop;
// ...
method Pop$Pre;
begin
if
Self.IsEmpty then
raise new Exception;
end;
end;
  1. En realidad, Pop$Pre sería un método estático dentro de una clase estática anidada, que recibiría un puntero de tipo IStack[X].
  2. Observe el truco del nombre: como el dólar no es aceptado dentro de un identificador, el programador no tendría acceso directo al método. Es un truco bastante usado, incluso por el compilador de C#.
  3. Toda clase que implementase IStack[X], siempre que se programase en Freya, añadiría automáticamente una precondición a su implementación de Pop, que ejecutaría el método Pop$Pre sobre el objeto activo convertido en el tipo de interfaz.
¿La ventaja de anidar la clase auxiliar dentro de la interfaz? Hasta ahora, Freya generaba una clase interna independiente... y tenía que cumplir con un estricto protocolo de asignaciones de nombres para detectar estas clases al leer un ensamblado generado en otro proyecto Freya. Con esta idea, por el contrario, la asociación entre la interfaz y la clase auxiliar es inmediata.

Etiquetas: , , ,

lunes, enero 07, 2008

DebuggerDisplayAttribute

Un truco sencillo que acabo de descubrir. Observe la imagen:
Depurando, si paso el ratón sobre el parámetro map, que pertenece a un tipo TypeMap que he definido, se muestra el número de elementos almacenados por este contenedor. ¿Cómo se logra? Parece sencillo: basta con redefinir el método ToString en la clase TypeMap. Y es verdad que eso funciona... pero hay una alternativa que muchas veces es mejor. Se trata de decorar la clase con el atributo DebuggerDisplay:
[DebuggerDisplay("Items = {items.Count}")]
internal sealed class TypeMap
Observe que el parámetro pasado al atributo es una especie de cadena de formato que permite incluir expresiones entre llaves. En este caso, mostramos el valor de la propiedad Count de un campo de TypeMap llamado items.
¿Porque me alegra tanto tener una alternativa a la redefinición de ToString? Porque así puedo separar con más claridad la lógica de la aplicación del código auxiliar que sólo necesito durante la depuración del proyecto. En el compilador de Freya, por ejemplo, los símbolos de métodos redefinen ToString para facilitar la búsqueda en la tabla de símbolos, pero otras clases lo hacían sólo por conveniencia, para mostrar información durante la depuración. Con el listado delante, me costaba normalmente decidir si determinada reescritura de ToString era necesaria o prescindible.

Etiquetas: ,

sábado, enero 05, 2008

UpdateBatchSize

MarshalingTodo programador que haya trabajado con SQL Server sabe lo importante que es empaquetar cuantas instrucciones sea posible dentro de una misma petición al servidor. El mismo programador, cuando comienza a trabajar con ADO.NET tropieza con la paradoja de que cada actualización de registros modificados en un dataset, debe viajar al servidor SQL completamente sola. Al menos, en la versión 1.1 era esto lo que ocurría...

Tinieblas

El problema, aclaro, no es grave. Típicamente, cuando se producen actualizaciones a través de modificaciones en un DataSet, estas modificaciones afectan a muy pocos registros, y el peaje a pagar es poco. Por el contrario, si usted se despierta un día actualizando centenares de registros a través de un data adapter, corra a visitar a su psicólogo de cabecera. Para actualizaciones masivas, es mejor usar directamente SQL, y si es posible, mediante procedimientos almacenados. Ese mítico escenario del feliz programador OOP que transparentemente modifica instancias de clases generadas por un ORM es una gran mentira: en una aplicación real, la mayoría de las modificaciones que no se realizan en el servidor mediante procedimientos almacenados, se generan a petición de controles enlazados a datos... a no ser que usted sienta fobia hacia el enlace de datos y los procedimientos almacenados.
De todos modos, las clases SqlDataAdapter y OracleDataAdapter de .NET 2.0 introdujeron una propiedad UpdateBatchSize, que permite agrupar varias actualizaciones en una misma petición. Si la propiedad vale 1, que es su valor por omisión, el comportamiento de los adaptadores es el de la versión anterior: una petición separada para cada registro actualizado. Un 0 en la propiedad provoca que todas las actualizaciones se sumen y se envíen en un único viaje de ida y vuelta. Y, finalmente, cualquier valor mayor que 1 sirve para crear grupos de peticiones del tamaño indicado.
Confieso, sin embargo, que hasta hace muy poco, nunca había usado esta propiedad. El motivo tiene que ver con la mecánica de las actualizaciones en ADO.NET. Suponga que tratamos con un una tabla cuya clave primaria es una columna autoincremental, y que tiene un campo de tipo rowversion/timestamp para el control optimista de concurrencia. Las inserciones en esta tabla se ejecutan mediante el siguiente grupo de instrucciones SQL:
insert into Clientes(...) values (...);
select IDCliente, TS from Clientes
where IDCliente = SCOPE_IDENTITY()
En otras palabras: cuando se inserta un registro, inmediatamente después se pide la última identidad creada al servidor, y el valor de la versión de fila para la nueva fila. Estos dos valores se devuelven al adaptador como un recordset de una fila, y el adaptador utiliza estos valores para poner al día las correspondientes columnas en el dataset.
¿Qué ocurriría si agrupásemos varias de estas instrucciones en un mismo batch o grupo de consultas? Evidentemente, el mecanismo saltaría por los aires. El servidor respondería enviando una colección de recordsets, pero el adaptador sería incapaz de distinguir cuál correspondería a cada fila. Es natural que el programador se abstenga de usar UpdateBatchSize, pues aparentemente obligaría a utilizar otro sistema para la puesta al día de la copia local de los datos tras una actualización.

Fiat lux

Pero, ¡pobre de mí!, no sospechaba cuán listos habían sido los que añadieron esta característica a los adaptadores. Hace poco, haciendo pruebas, me di cuenta de que un adaptador con UpdateBatchSize parecía seguir enviando consultas individuales al servidor SQL. Al menos, en el SQL Profiler se mostraban las inserciones como instrucciones individuales. Intrigado, busqué más información en Internet, y así supe que la labor de UpdateBatchSize no consiste en generar una única petición formada por una gigantesca instrucción. La técnica es más sofisticada, y según lo que sé, consiste en agrupar todas las peticiones en un único paquete RPC (Remote Procedure Call). Esto ahorra un ancho de banda considerable, y lo que es más importante, disminuye los problemas de latencia provocados por la red. No se optimiza la operación, considerada por separado, al máximo, pero se logra un mejor resultado, de todos modos... y abre el camino a la esperanza.
Volví entonces a la documentación de SqlDataAdapter y descubrí un pequeño detalle: cuando BatchUpdateSize es distinto de 1, los comandos del adaptador no pueden tener los valores Both o FirstReturnedRecord en su propiedad UpdateSource. Es decir: no se puede usar un recordset para poner al día la copia local. Pero esto también significa que se admite la opción OutputParameters. Si la actualización se realiza a través de procedimientos almacenados, ¡es posible usar UpdateBatchSize y seguir actualizando la copia local en una misma operación!
En estos casos, hay que tener un procedimiento almacenado en el servidor parecido a éste:
create procedure InsertCliente
@IdCliente int output,
@TS timestamp output,
@Nombre T_NOMBRE,
@Apellidos T_APELLIDOS as
insert into Clientes(Nombre, Apellidos)
values (@Nombre, @Apellidos);
select @IdCliente = IDCliente, @TS = TS
from Clientes
where IDCliente = SCOPE_IDENTITY()
Observe que esta vez no se utiliza un recordset, sino los parámetros de salida del procedimiento, para devolver los valores actualizados de las columnas modificadas en el servidor.
Evidentemente, el uso de procedimientos almacenados para estas operaciones requiere un poco más de planificación por parte del programador... pero es una tarea mecanizable, y seguramente podremos contar con la ayuda del DBA. Tenga en cuenta que el DBA no protestará si le pide que no permita actualizaciones en una tabla excepto a través de un procedimiento almacenado: estas cosas hacen feliz a un administrador de bases de datos.
Es poco probable que una aplicación en dos capas, para la que se presume poca carga, se beneficie de un mecanismo tan sofisticado de actualización. Pero en una aplicación multicapas con mucha carga, cada milisegundo (¡sí, hablo de milisegundos!) que se gane en una operación, significa más capacidad de concurrencia para la capa intermedia: el modelo de concurrencia de una capa intermedia con pooling no ofrece capacidad lineal respecto a los tiempos de respuesta. El truco de combinar UpdateBatchSize con procedimientos almacenados puede parecer excesivamente complicado a algunos, pero es bueno saber que disponemos de un arma como ésta en nuestro arsenal.

Actualización: He subido una pequeña actualización de Intuitive C#, con una nueva sección sobre métodos de extensión. Lo único interesante es que he añadido la sección al capítulo de interfaces.

Etiquetas: , ,

jueves, enero 03, 2008

El objeto activo

¿Qué se supone que tiene que pasar cuando se ejecute este código?
using System;
public
Program = class
public
method Test: Boolean => Self <> nil;
        static method Main;
begin
var Instance: Program := nil;
Console.WriteLine(Instance.Test);
end;
end
end

Janie's got a gunLa respuesta breve es que se imprimirá False.
La respuesta larga tiene que ver con uno de los objetivos de Freya: experimentar con la eficiencia del lenguaje, y en este caso, entra en escena el código IL que se genera para la llamada a un procedimiento. Hay dos de estos códigos: call y callvirt. El segundo de ellos se utiliza, evidentemente, para ejecutar métodos virtuales, pero curiosamente, también puede ocuparse de métodos no virtuales, como es el caso de nuestro método Test. En tal caso, la diferencia respecto a call es que, al generarse el código nativo final, callvirt añade una verificación del puntero de instancia. Si este es nulo, excepción al canto.
Resulta que callvirt es utilizado deliberadamente por C# para casi todas las llamadas a métodos de instancia, sean virtuales o no. Esta es la forma de satisfacer la especificación del lenguaje, que exige que rueden cabezas si se pasa un nulo como objeto activo a un método de instancias. Es también interesante que, en contadas ocasiones, C# puede optimizar la llamada si sabe a ciencia cierta que el puntero no será nulo. ¿Cuándo puede tener esta certeza? Por ejemplo, cuando el objeto pasado se acaba de construir. Un método ejecutado sobre una expresión new no necesita esta verificación.
Freya, por el contrario, siempre usa call para métodos no virtuales, con lo que logra una ganancia en velocidad mínima, pero notable, sobre C#. Hay una ganancia también en el tamaño del código nativo, que se traduce en un uso más eficiente de la memoria, y a la larga, en más velocidad: la comprobación de no nulidad la ejecuta quien llama, por lo que se repite innumerables veces a lo largo del código. El coste: la especificación de Freya no exige que la instancia activa sea no nula. De ahí que tengan sentido ejemplos como el mostrado.
La pregunta es, naturalmente, si es peligrosa esta "relajación". No lo es, por supuesto:
  1. Si se pasa una referencia nula como objeto activo a un método que no lo permite, la excepción se va a producir, de todos modos, en cuanto el objeto activo intente tocar algún campo o método de instancia. Es decir: el error se detectará, aunque algo más tarde.
  2. En realidad, la no nulidad del objeto activo puede verse como una precondición en el contrato del método, en los lenguajes que la exigen: se supone que el software correctamente escrito nunca violará la precondición. Pero en ese caso, si en el software final se mantiene la verificación, ¡estaremos comprobando una y otra vez si se produce lo imposible!
A mediano plazo, Freya deberá incorporar aserciones especiales sobre la nulidad de campos y parámetros. Esto traerá algunas novedades interesantes al lenguaje. Piense un momento: si digo que el campo C, de tipo Object, no puede ser nulo, ¿cómo vamos a tratar el hecho de que el CLR lo inicialice precisamente como nulo? La clave está en añadir inicializadores de campos a los constructores, al estilo C++:
// En una clase Stack[X]...
constructor(Capacity: Integer) :
Items(new Array[X](Capacity)),
Inherited
begin
end
;
El CLR permite que estas inicializaciones de campos se ejecuten incluso antes de la llamada al constructor de la clase base: es así, de hecho, como se ejecutan los actuales inicializadores de campos de C# y Freya. La diferencia es que, con el nuevo recurso, los inicializadores de campos podrán utilizar los parámetros del constructor, algo que ahora mismo es imposible.
Por cierto, el último listado puede abreviarse a lo siguiente:
constructor(Capacity: Integer) :
Items(new Array[X](Capacity));
La llamada al constructor por omisión heredado se asume, y el bloque de instrucciones, al estar vacío, se puede omitir. Un constructor paralelo, sin parámetros, se programa en Freya de este modo:
constructor : Self(128);
Las instancias especiales base y this de C# se sustituyen en Freya por los identificadores especiales Inherited y Self.

... quien haya depurado una aplicación .NET en código nativo, se habrá encontrado con instrucciones aparentemente absurdas como ésta, justo antes de una llamada a rutina:
cmp byte ptr [rax],0
Se trata de la verificación de no nulidad, en este caso, en código de 64 bits. En un procesador de 32 bits, he visto esta variante:
cmp byte ptr [eax],eax
Al parecer, la diferencia debe estar en el tamaño de ambas instrucciones. Pero en ambos casos, el principio es el mismo: si EAX o RAX tienen un puntero no nulo, la instrucción hará una comparación rápida e inofensiva. En caso contrario, la memoria a la que apunta el registro estará en la zona prohibida, y el propio procesador levantará la liebre... quiero decir, la excepción.

Por último, la posibilidad de simular el traspaso de un objeto activo nulo la dan los métodos de extensión:
public static class StringExtender
{
public static bool IsEmpty(this string s)
{
return string.IsNullOrEmpty(s);
}
}
Si se puede lograr por medio de métodos de extensión, ¿qué hay de malo en permitirlo para los métodos de instancia?

Etiquetas: , ,