lunes, febrero 09, 2009

Garantías

Dorothy se enfrenta a la familia política del León CobardeLa Programación no es trabajo ni para cobardes ni para desmemoriados. La máquina es una bestia salvaje atada por una cadena: si queremos ganarnos la vida con ella, tenemos que despojarnos del miedo irracional. Pero también tenemos que recordar, en todo momento, cuál es la longitud de la puñetera cadena.
Cuando hablo de "cadena", usted debe entender "contrato": cada pieza de software debe dejar bien claro qué es lo que hace por nosotros, siempre que le proporcionemos lo que especifica el contrato. Math.Sqrt nos calculará la raíz cuadrada de un número, siempre que le proporcionemos un número no negativo. El indexador de la clase genérica Dictionary nos devolverá el valor asociado a una clave, aunque sólo cuando la clave se haya almacenado antes. Ignorar la existencia de estos contratos nos puede llevar a uno de dos problemas:
  1. Pedimos la raíz cuadrada de menos uno, y el león nos come una oreja.
  2. El problema anterior suele pasar una sola vez. El aprendiz de domador suele quedar aterrorizado, y de ahí en adelante no se acerca al león, excepto cuando su abogado está presente (para que se coma al león en caso necesario). El abogado, por desgracia, termina siendo un problema mayor que el propio león.
A continuación, voy a presentarle varias joyas que muestran los problemas que se pueden presentar cuando el programador decide ignorar, por excesivo terror, las garantías que le ofrecen los contratos. El problema con estas "joyas" es que, cuando uno se las encuentra, pierde un tiempo valioso intentando decidir si:
  1. Se trata de una genialidad.
  2. O se trata de una simple cagada.
El 99% de los casos corresponde a la segunda opción, pero uno se niega a perder tan pronto la fe en el género humano.
La primera perla negra tiene que ver, como no podía ser de otro modo, con la ya mencionada clase Dictionary. Estoy harto de ver este fragmento de código en el software de cierta empresa imaginaria:
if (dict.ContainsKey(key))
dict[key] = value;
else
dict.Add(key, value);
Esto podría pasar como buena programación en Ankh-Morpork, pero aquí solemos ser un poco más exigentes. Un buen día, uno de los leones, el método Add, se adelantó al gato y le comió la lengua al programador, cuando éste intentó insertar una clave duplicada en un diccionario. De ese día en adelante, el programador pincha primero el diccionario con una vara larga, para ver si ya existe la clave. En caso afirmativo, actualiza el valor asociado a la clave; de lo contrario, inserta el nuevo par clave/valor. El problema es que el mismo resultado se obtiene con menos charla:
dict[key] = value;
En efecto, cuando se realiza una asignación sobre el indizador un diccionario, si exista la clave, se actualiza el valor; de lo contrario, se inserta. Hay dos posibilidades: o el programador lo sabía, y se le olvidó... o nunca se tomó la molestia de leerse la documentación. Prefiero pensar lo primero. Al fin y al cabo, la memoria viene genéticamente determinada, y uno no debe reñirle al prójimo por sus genes.
Por cierto, y si me permite el paréntesis, ¿sabía usted que podemos inicializar un diccionario genérico como si se tratase de una vulgar colección? ¡Es que un diccionario genérico es una colección! El quid está en saber qué tipo de colección: sus elementos son instancias de la clase genérica KeyValuePar. Esta es la técnica:
private Dictionary<int, string> d =
new Dictionary<int, string>()
{
{1, "Uno"},
{2, "Dos"},
{3, "Tres"},
};
Preste atención a la coma, aparentemente superflua, que sigue al tercer par de valores. Se trata de un sencillo, pero poco conocido, truco sintáctico que permite añadir o quitar pares, en cualquier momento, sin preocuparnos por el carácter especial del último par. Este mismo truco se aplica a los inicializadores de objetos, e incluso a la declaración de tipos enumerativos. Si le parece una barbaridad y una concesión (hay gente para todo), recuerde que en C# el punto y coma es un terminador, no un separador. ¿Qué hay de malo en querer usar la coma también como terminador, en estos casos?
Vamos ahora a otra garantía que solemos olvidar... o simplemente desconocer. Esta vez, la garantía tiene que ver con el propio lenguaje:
double d = 0.0;
if (cond)
d = 1.0;
else
d = 2.0;
Naturalmente, la inicialización de la variable local sobra:
double d;
if (cond)
d = 1.0;
else
d = 2.0;
No pasa nada porque se deje d sin inicializar... aparentemente. La instrucción que sigue lo garantiza, sin importar cuál de sus ramas se ejecute. Por supuesto, en este caso sencillo, habría sido más fácil escribir todo el código de esta manera:
double d = cond ? 1.0 : 2.0;
El programador que escribió el primer fragmento ha olvidado la sección de la especificación de C# que explica el concepto de asignación definida. La ha olvidado, o nunca se ha tomado la molestia de leerla.
Otro disparate frecuente debido al temor a lo desconocido:
private double f()
{
double d = 0.0;
OpenXXX();
try {
if (cond)
d = f1();
else
d = f2();
}
finally {
CloseXXX();
}
return d;
}
En realidad, el código anterior combina varios disparates debido al olvido o desconocimiento de las garantías de los contratos. Para empezar, vuelve a sobrarnos la inicialización de la variable local: la rama try del bloque try/finally nos garantiza la asignación definida de la variable, y la rama finally es irrelevante, si no utilizamos d en su interior. Tenga en cuenta que, si se ejecuta el finally, nunca llegaremos a la instrucción return.
Pero la zarpa que realmente aterra al programador del ejemplo es la posibilidad de ejecutar un return dentro de la rama try, por desconocimiento (u olvido) del funcionamiento de esta instrucción. Podemos ejecutar ese return sin problemas, y el código se simplifica entonces enormemente:
private double f()
{
OpenXXX();
try {
return cond ? f1() : f2();
}
finally {
CloseXXX();
}
}
El código simplificado es más corto, más comprensible y, probablemente, algo más eficiente, y ya no hablemos de la elegancia. ¿Moraleja? Conozca sus deberes y derechos, tal y como los especifican los contratos de su software, y así será mejor programador. O, mejor aún: RTFM (again).

Etiquetas: , ,

10 Comments:

Blogger AndyMaster said...

Hola Ian.

No me queda claro el tema de la coma al final del inicializador de la colección. Tienes algún link donde pueda profundizar sobre eso? ya que lo encuentro en la especificación del lenguaje pero no se hacen comentarios sobre eso.

martes, febrero 10, 2009 8:08:00 p. m.  
Blogger Ian Marteens said...

Hola, Mauricio:

Va a ser un poco complicado, porque la información está en el fichero Word de la especificación. Pero te copio un par de secciones con la sintaxis. Por ejemplo, con las declaraciones de enumerativos (página 391):

enum-declaration:
attributesopt enum-modifiersopt enum identifier enum-baseopt enum-body ;opt
enum-base:
: integral-type
enum-body:
{ enum-member-declarationsopt }
{ enum-member-declarations , }


Observa la coma en la segunda variante de enum-body. Lo mismo pasa con los collection initializers (pag 169) y los objects initializers (pag 167).

(es una mierda que Blogger no soporte la etiqueta PRE; para que luego alaben a Google)

miércoles, febrero 11, 2009 1:17:00 p. m.  
Blogger Alfredo Novoa said...

recuerde que en C# el punto y coma es un terminador, no un separador. ¿Qué hay de malo en querer usar la coma también como terminador, en estos casos?

Pues que la coma en C# era un separador y no un terminador. Si quieren usar un terminador que usen un punto y coma y que no hagan esa chapuza.

jueves, febrero 12, 2009 12:44:00 p. m.  
Blogger Alfredo Novoa said...

Por supuesto, en este caso sencillo, habría sido más fácil escribir todo el código de esta manera:

double d = cond ? 1.0 : 2.0;


El problema es que esta sintaxis es horrible y cuesta acostumbrarse a ella a los que no vienen de C.

Con lo sencillo que hubiese sido permitir esto:

double d = if (cond) 1.0 else 2.0;

jueves, febrero 12, 2009 12:49:00 p. m.  
Blogger Ian Marteens said...

que no hagan esa chapuza

:) Eso es porque, al igual que el menda, tienes el don de la memoria, y te es fácil recordar dónde van el uno y el otro.

Es cierto, de todos modos, que para aprovechar esta "regla especial", tienes primero que acordarte de ella.

Con lo sencillo que hubiese sido permitir esto:

Como en Freya (excepto que Freya exige un "then"). La verdad es que desde los viejos tiempos de John Backus, la sintaxis de expresiones ha mejorado muy poco en los lenguajes imperativos.

viernes, febrero 13, 2009 11:37:00 a. m.  
Blogger Al González said...

¡Hola Ian!

Sobre las sintaxis de lenguajes, sería interesante que toda sentencia de asignación fuese al mismo tiempo una expresión que, usada como tal, devolviera el valor asignado. Desconozco qué lenguajes así lo permiten (algo vi alguna vez, mas no recuerdo dónde), pero imagino código como este en los pascales:

If Result := A Then

Repeat
...
Until (A := A + 1) = 10;

¡Saludos!

Al González. :)

domingo, febrero 22, 2009 9:06:00 a. m.  
Blogger Ian Marteens said...

Hola, Al:

Eso lo hacen C/C++/Java/C# ... y la verdad es que las consecuencias no son muy buenas. Por una parte, la ganancia en eficiencia, al menos en JVM y CLR, es discutible, y es más fácil que el compilador JIT se ocupe del asunto. Lo sé porque he experimentado bastante con este asunto en XSight: en la mayoría de los casos, el código final que se consigue es peor.

Por la otra, complica bastante la lectura del código y su posterior mantenimiento.

domingo, febrero 22, 2009 6:31:00 p. m.  
Blogger Andrés Ortíz said...

Hola, considero que hay algo mal, OpenXXX() debería estar dentro del Try, si eso es el intento de abrir un fichero, conexión a BD, etc, evidentemente es propicio de sufrir alguna excepción. Realmente veo la regla un tanto incompleta.

viernes, julio 10, 2009 5:25:00 p. m.  
Blogger Alfredo Novoa said...

Andrés, es que en ese ejemplo se supone que esa excepción se capturaría en otro sitio.

viernes, julio 10, 2009 5:31:00 p. m.  
Blogger Al González said...

considero que hay algo mal, OpenXXX() debería estar dentro del Try, si eso es el intento de abrir un fichero, conexión a BD, etc, evidentemente es propicio de sufrir alguna excepción. Realmente veo la regla un tanto incompleta.

No hay nada mal, Andrés, el patrón Try-Finally es "Apertura-Try-Uso-Finally-Cierre". La rutina OpenXXX debe estar construida con lo necesario para no terminar con la "apertura" hecha (ni siquiera parcialmente) si dentro de ella ocurrió un problema. Internamente debe deshacer lo que haya alcanzado a realizar si ocurre una excepción y además a) propagar dicha excepción o b) devolver un estado que permita a la rutina llamadora ejecutarlo así: "If OpenXXX() = OK Then". En ambos casos la rutina llamadora debe evitar el "cierre", puesto que no hay cierre qué hacer. De lo contrario, la llamada a CloseXXX podría ejecutar una operación inválida, derivando en una excepción más.

No hay nada de incompleto en el ejemplo escrito por Ian.

Saludos.

Al González. :)

viernes, julio 10, 2009 7:06:00 p. m.  

Publicar un comentario

<< Home