sábado, noviembre 29, 2008

Consenting adults, consenting ducks

No le toques los colmillos al Conde DuckulaHace poco, escribía en los comentarios de otra entrada que esto la Informática no es una ciencia, sino una ingeniería... y Alfredo Novoa respondía que ni siquiera eso. La siguiente anécdota demuestra cuán cierta es, desgraciadamente, esta afirmación.
La historia viene a cuento de una característica "novedosa" llamada duck typing. Según sus defensores, si una clase tiene métodos Add, Remove e Insert, y un indexador, ¿qué hay de malo en asignar una instancia de la clase a una variable de tipo ICollection? Eso es, en pocas palabras, el duck typing: si algo camina como un pato, nada como un pato, vuela como un pato (es decir, las tres cosas las hace mal) y además caga como un pato (es decir, constantemente), entonces, según los adeptos del mecanismo, tiene que ser un pato. Naturalmente, yo digo que puede ser una oca, o un pato robot, o el conde Duckula. No es buena idea tratar al señor conde como a un vulgar pato. Y así lo hice ver aquí, aunque de pasada:
Algún tiempo más tarde, alguien se quejó en un comentario... y como no suelo llevar cuenta de los comentarios tras cierto tiempo, no me percaté del hecho hasta hace un par de días. Esta es el comentario:
Cuál es el problema que le ves al "duck typing". Los que lo utilizan, suponen que los programadores son "consenting adults".
Consenting adults, eh...
En principio, eso serviría también para justificar las conversiones de tipos bestiales que son el pan de cada día en C++. Al fin y al cabo, si yo sé que un puntero se representa como un entero de 32 bits en mi plataforma, ¿por qué no voy a poder incrementar ese entero en cuatro para acceder al siguiente elemento de un vector? Consenting adults. No estamos locos. Sabemos lo que queremos...
Ahora bien, ¿es tan peligroso el duck typing como las conversiones bestiales? Veo dos problemas principales:
  1. Una pila no es simplemente un objeto que tiene dos métodos Push y Pop. Esos dos métodos, además, tienen que satisfacer determinado contrato. En la práctica, si el pato es mío, es un descuido mío el que no haya hecho que la clase correspondiente implemente la interfaz deseada. Si el pato es ajeno, es muy peligroso asumir que puedo tratar a un águila como a un pato simplemente porque ambos tienen pico.
  2. Si el pato es ajeno, además, los problemas con el versionado de clases pueden ser peliagudos. O "plumiagudos", si lo prefiere...
En cualquier caso, me hace gracia el uso de la teoría de los consenting adults. Hace poco, en Alemania, un buen señor pidió a otro consenting adult que le cortase la picha y se la comiese (en ese orden). El otro, efectivamente, cumplió con lo pactado. Luego mató a la víctima, como ésta había solicitado, se comió un muslo, y puso el otro a secarse para hacer jamón. Al caníbal lo pillaron, y lo llevaron a juicio. E inmediatamente, se armó el correspondiente revuelo: ¿por qué condenarlo? ¿No se limitó a hacer lo que el otro pirado le pedía? ¿No eran una parejita de consenting adults?
Evidentemente, tengo mi propia opinión al respecto. Yo, al canibalizado, si lo pillase antes de ser devorado, no le haría nada. Al fin y al cabo, si quiere que otro lo mate y se lo zampe, es su problema. Es, en efecto, un consenting adult: se supone que sabe lo que hace. Mi problema es con el caníbal: es un pirado, y tengo serias sospechas de que intentará repetir la acción. La próxima vez, no estoy seguro de que vaya a encontrar un consenting adult para satisfacer sus antojos culinarios. Es un individuo peligroso. No sé si eso bastará (junto con lo que ya ha hecho) para enviarlo a la cárcel, pero en mi barrio no lo quiero.
Lo mismo se aplica a los duck typers. ¿De manera que usted es aficionado a transformar el agua en vino, perros en patos y patos en borricos? Ah, claro, usted sabe lo que hace. Pero lo que usted hace es peligroso. No me gustaría verle enredando con mis proyectos...

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, diciembre 31, 2007

Ornitorrinco

... and the mome raths outgrabe.No todo lo que camina como un pato, nada como un pato y pone huevos como un pato, tiene necesariamente que ser un pato. Podría tratarse de un ornitorrinco, ¿verdad?
Suponga que yo le digo que esto es Freya:
IStack = interface[X]
Count: Integer;
Top: X;
method Push(Value: X);
method Pop;
requires Count > 0;
    IsEmpty: Boolean => Count = 0;
end;
Para que le resulte más fácil detectar el elemento extraño, utilizaré una sintaxis equivalente, algo más verbosa:
IStack = interface[X]
property Count: Integer;
property Top: X;
method Push(Value: X);
method Pop;
requires Count > 0;
    property IsEmpty: Boolean;
begin
Result := Count = 0;
end;
end;
Y ahora, antes de subir la otra mitad del artículo, dígame si lo anterior le parece bien o mal...

Efectivamente, como mencionaba Marto en un comentario, se supone que las interfaces no "implementan". Sin embargo, es precisamente ese efecto lo que logran los métodos de extensión de C# 3.0:
interface IStack<X>
{
int Count { get; }
X Top { get; }
void Push(X value);
void Pop();
}
static class StackExt
{
public static bool IsEmpty<X>(this IStack<X> s)
{
return s.Count == 0;
}
}
Gracias a estos métodos, es posible escribir código como el siguiente, suponiendo que tenemos ya una clase que implementa el tipo de interfaz:
IStack<int> st = new StackImpl<int>();
if (st.IsEmpty()) { ... }
¿Es bueno, o es malo? No veo nada malo en permitir manipulaciones predefinidas sobre el estado "público" ofrecido por el contrato de la interfaz: lo mismo se puede lograr, incluso sin métodos de extensión, aunque al precio de escribir muchísimo más.
Ahora bien, mi duda va más allá. ¿Merece la pena implementar métodos de extensión (Freya ya los tiene), o sería suficiente con añadir al lenguaje implementaciones predefinidas en interfaces, como la del ejemplo de Freya? El caso es que los métodos de extensión son nada elegantes:
  • El mecanismo de selección es horrible: basta con introducir una cláusula using para activarlos, dándole un protagonismo inapropiado a estas cláusulas.
  • Estos métodos, cuando se emplean para extender una clase, plantean un grave problema de estabilidad respecto al versionado: si el implementador de la clase introduce un método de instancia en la clase extendida, dejará de funcionar la extensión, porque el método de instancia tiene la prioridad. Lo peor es que sería muy difícil detectar estos problemas.
De momento, a falta de más análisis, me inclino por sustituir este recurso por implementaciones en interfaces. Es verdad que no son recursos equivalentes, pero la suciedad de los métodos de extensión me aterra.

Etiquetas: , , ,

lunes, marzo 05, 2007

Intermezzo técnico

¿Me permite un pequeño descanso antes de continuar con la serie sobre ideologías? En el último post sobre Freya, en los comentarios, Daniel Alvarez me señalaba unos artículos en Internet sobre integración con Visual Studio, en la Bitwise Magazine inglesa. Me llamó la atención, merodeando por dicha página, el siguiente artículo:
Cuidado, no estoy diciendo que esté de acuerdo con todo lo que se dice en el artículo. En realidad, creo que las opiniones de Huw y Dermot son muy diferentes, y en ocasiones incluso hablan de cosas muy diferentes sin darse cuenta.
Lo que quiero destacar es la importancia que Dermot concede al uso de interfaces... aunque en su caso, se refiere concretamente a las interfaces COM. Destaco esta opinión (basada en la práctica) porque coincide con lo que he podido comprobar por cuenta propia en estos últimos años... y que difiere radicalmente de las recomendaciones de Microsoft sobre el uso de tipos de interfaz.
En concreto, me gusta plantear la funcionalidad y arquitectura de un sistema mediante un conjunto pequeño de interfaces. Luego, las clases entran en escena, pero sólo como un recurso de implementación. Por ejemplo, el compilador de Freya está basado en unas pocas interfaces como ISymbolTable, ICodeGenerator e IParser que son luego implementadas por clases que funcionan como singletons dentro de una instancia del compilador. Luego tenemos interfaces como IAstNode, IStatement e IExpression, que son implementadas luego por una larga colección de clases. Lo importante es que toda comunicación entre módulos se especifica por medio de tipos de interfaz: el canal de comunicación que abren las interfaces es mucho más estrecho que el de las clases. Y la misma situación se da en Proteus, en XSight RT...
¿Mi consejo? Lea, analice, experimente y compare. Y luego, cuando decida, cuéntenos su decisión.

Etiquetas: , ,