En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

jueves, 27 de septiembre de 2012

¿Variables globales? No, Gracias
cómo usar las variables globales en C

Hoy vamos a tratar de destruir otro de los pilares de la no-programación: la variable global. Ojo: las variables globales (así como el goto, por ejemplo) son parte del lenguaje, entonces existen: a veces es posible y/o necesario usarlas. Pero, al igual que el goto, casi siempre es posible prescindir de éllas, con grandes beneficios de estilo (legibilidad y facilidad de mantenimiento, sobre todo) y funcionalidad (menos bugs): Llámalo nada.

Los puntos críticos son muchos, pero ya que no quiero escribir ni un poema ni un libro sobre el tema, he aislado unos pocos. Vamos a ver:

1) Las variables globales no son thread-safe: en la programación multihilo el uso de variables globales puede generar unos mal funcionamientos sutiles y difíciles de encontrar. El caso clásico es la instrucción de lectura/escritura de una global fuera de un área crítica, o sea (por ejemplo) sin la protección de un mutex: en un proyecto de gran envergadura es suficiente un olvido (sólo uno!) de este tipo para darte tantos dolores de cabeza que dejaras de usar las globales para el resto de tu vida como programador. Ver para creer. En este punto, se podría objetar: "Pero yo estoy escribiendo programas normales, no multihilo". Muy bien, declarando previamente (y matizando en la siguiente sección) que un programa "normal" se puede considerar un programa multi-hilo con un solo hilo, doy las gracias por la objeción, que me permite de lanzar el punto 2:

2) las variables globales violan el principio de mantenimiento/reutilización del Software: cuando se escribe profesionalmente Software siempre se debe pensar en el trabajo en equipo, y por lo tanto, en las operaciones de mantenimiento que podrían ser realizadas por otras personas (y, a veces) después de un largo tiempo. Evidentemente, un código, vasto y lleno de globales, es difícil de mantener como un código No Comment (¿recordáis?), porque el historial de una global es poco comprensible, podría verse afectado en muchos lugares diferentes por muchas funciones diferentes, y si para entender un trozo de código tienes que abrir decenas de archivos... os habeis ganado otro dolor de cabeza! Y no hablamos de reutilizar código lleno de globales para otro proyecto: si no lo habéis hecho nunca por lo menos tratar de imaginar la dificultad. Y no sólo: volvemos al punto 1 (thread-safe): ¿quién me dice que el codigo "normal" no tiene que volverse a utilizar (un día) en un proyecto multihilo? Si el código es thread-safe se puede hacer fácilmente, pero si hay globales de por medio... Bueno, buen trabajo (y buena suerte).

3) las variables globales aumentan la dificultad de depuración y multiplican la probabilidad de errores de programación: para la depuración, remito al punto 2: si el valor de una variable es difícil de seguir en términos de mantenimiento también lo será a nivel de depurar. Y habrá más mal funcionamientos para depurar (fantástico!), porque, además de todos los posibles errores de codificación. hay que añadir los de scope: probar este código:
int my_var = 0;

void incrementaMyVar()
{
    my_var += 5;
}

int main(int argc, char **argv)
{
    // hago mil cosas...
    // ...

    // incremento my_var
    incrementaMyVar();

    // hago otras mil cosas...
    // ...

    // declaro una "nueva" variable my var y la uso
    int my_var = 2;        // ah, ah, ah: redeclaración!
    // ...

    // hago todavía mil cosas...
    // ...

    // incremento y test my_var (oops! cual my_var?)
    incrementaMyVar();
    if (my_var == 2)
        formatMyHardDisk();    // uh, uh, uh: era la equivocada!
    // ...

    return 0;
}
El que se ha enseñado arriba era un problema de scope con redefinición local (accidental) de una global. El código que sigue es aún más simple, muestra un descuido de un detalle importante:
int my_var = 0;

void hagoMilCosas()
{
    // hago mil cosas...
    // ...

    // incremento my_var (escondido entre mil instrucciones!)
    my_var += 5;

    // hago otras mil cosas...
    // ...
}

int main(int argc, char **argv)
{
    // hago unas cuantas cosas...
    // ...

    // llamo hagoMilCosas()
    hagoMilCosas();    // oops! he incrementado my_var sin quererlo

    // hago otras cosas...
    // ...

    // test my_var
    if (my_var == 5)
        formatMyHardDisk();    // uh, uh, uh: me he equivocado?
    // ...

    return 0;
}
buena historia, eh?

4) las variables globales violan el principio de encapsulación de variables: bien, esto ni siquiera es necesario que lo explique, una global es cualquier cosa menos que encapsulada... oops, pero esto es OOP, entonces un poco fuera del tema del blog: perdón, quería exagerar. Bueno, ya que estamos hablando de OOP, y entonces de C++, cito con gran placer el grande M.Cline que en su C++FAQ dice (traduzco, vale);
El nombre de una variable global debe empezar con //.

Esta es la forma ideal de declarar una variable global:

// int xyz; <-lo que hace ideal esta global es la inicial //

Esta es la manera ideal de utilizar una variable global:

void mycode()
{
  ...
  // hace_algo_con(xyz);  <-ídem
  ...
}

OK, esto es un juego. Casi. La verdad es que hay casos en que las
variables globales son menos peores que la alternativa - cuando las
globales son el menor de los males. Pero ellas son siempre malvadas.
Así que lavarse las manos después del uso. Dos veces.
Palabras santas.

Sin embargo, si pensáis que todo esto es sólo harina de mi bolsa, intentar pedirle a nuestro amigo Google: global variable are evil? y vais a ver que avalancha de resultados sale. Si estáis interesados en una de las páginas más interesantes la podéis encontrar aquí.

Creo que lo que se ha dicho es suficiente. Obviamente estoy un poco sesgado porque he trabajado mucho sobre software multihilo (y todavía tengo un poco de dolor de cabeza ...), pero os pido que confíeis y difundáis el mensaje lo más posible. Sí, lo sé, decirle a un programador sin experiencia (o a un programador cansado) "no usar variables globales" es como decirle a un niño "no comer demasiados dulces": bueno, hay que hacerlo. La salud en primer lugar.

Hasta el próximo post.

domingo, 9 de septiembre de 2012

No Comment 2 - El regreso
cómo escribir los comentarios en C - pt.2

Hoy ha llegado el momento de completar el discurso del No Comment del que hablamos aquí. Se habló, por así decirlo, del nivel micro (los comentarios sobre los bloques de código), pero nos perdimos el nivel macro, es decir, el propio contenedor (el file) y los grandes bloques de código que son las funciones. Decidme la verdad: si trabajáis (o habéis trabajado) en un equipo, ¿cuántas veces habéis abierto un file escrito por un compañero (que pertenece a la religión No Comment, devoto y practicante) y habéis descubierto que la única pista sobre el código contenido era el nombre del fichero mismo? Y, ya que por la Ley de Murphy, el distinguido compañero estaba de vacaciones (justas y merecidas), habéis pasado el día entero sólo para entender de qué iba el file, y sólo en ese momento, habéis empezado a descifrar el código gracias a la Piedra de Rosetta.

Un file necesita un header y las funciones (al menos las más importantes, que suelen ser las globales) necesitan una cabecera hecha como dios manda (pero también las funciones estáticas se merecen un mínimo de presentación). Espero que el mensaje sea claro.

Bien, y ahora ¿cómo podemos hacer una cabecera? Aunque las posibles variaciones son infinitas, diría que, para empezar, algo muy sencillo como esto, podría funcionar:
/*!
 *  FILE
 *     getComments.c - extracción comentarios de un file C
 *  PROJECT
 *     myDoc - generación automática documentación
 *  FUNCTIONS
 *     globales:
 *        getComments()
 *     estáticas:
 *        formatLine()
 *  AUTHOR
 *     A.Abate
 *  OPERATING SYSTEM
 *     Linux (all versions)
 *  COMPILER
 *     GNU gcc
 *  RELEASE
 *     1.0 (Septiembre 2012)
 */ 
Obviamente se puede abundar más o menos en descripciones. Por ejemplo, si el archivo contiene el código de un protocolo de comunicación, se puede agregar un bloque de descripción (un dibujo en texto) con las entidades involucradas y el flujo de mensajes de comunicación.

En cuanto a las funciones importantes, se puede utilizar la sintaxis clásica de las Man Pages de Unix (y Linux), o sea, hacer algo como esto:
/*!
 *  NAME
 *     getComments - examina un file C para extraer los comentarios
 *  SYNOPSIS
 *     int getComments(
 *        char *inpath,
 *        char *oupath)
 *  DESCRIPTION
 *     Extrae los comentarios de un file C con pathname <inpath> y los
 *     escribe en un file de documentación con pathname <outpath>.
 *  RETURN VALUE
 *     0 a operación efectuada. Un valor negativo en caso de error
 */ 
Y, también aquí, uno puede abundar más o menos con las descripciones, añadiendo, por ejemplo, los detalles de todos los códigos de retorno, y, si es necesario, mediante la inserción de un apartado EXAMPLES adicional, con breves ejemplos de uso.

Como los mas observadores habrán notado, la primera línea de los dos ejemplos anteriores comienza con "/*!", que contiene un símbolo de reconocimiento para una posible herramienta para la auto-generación de la documentación (como Doxygen). Y aún los mas observadores habrán notado que el contenido de los dos ejemplos es acerca de la auto-documentación. El hecho es que he decidido ofrecer un caso real, y que podría ser mejor que escribir, ad-hoc, un instrumento de documentación simple que he escrito y probado en un muy poco tiempo (es realmente muy simple, pero puede ser mejorado) .

Pues bien, la mini-herramienta estaría (casi) toda aquí:
int getComments(
    char *inpath,
    char *oupath)
{
    char *line = NULL;
    size_t len = 0;
    int retcode;
    bool start_comment = false;

    // abro el file de input
    FILE *fpin;
    if (fpin = fopen(inpath, "r")) {
        // abro el file de output
        FILE *fpout;
        if (fpout = fopen(oupath, "w")) {
            // leo el file de input
            while (getline(&line, &len, fpin) != -1) {
                // test final comentarios para reset flag start_comment
                if (strstr(line, "*/") && start_comment) {
                    start_comment = false;
                    fprintf (fpout, "\n\n\n");
                }

                // test flag start_comment
                if (start_comment) {
                    // formateo y escribo la linea en el file output
                    fprintf(fpout, "%s", formatLine(line));
                }

                // test inicio comentarios para set flag start_comment
                if (strstr(line, "/*!"))
                    start_comment = true;
            }

            // libera recursos y set retcode=OK
            fclose(fpout);
            free(line);
            retcode = 0;
        }
        else
            retcode = -2;    // fopen err: set retcode=err

        fclose(fpin);    // libera recursos
    }
    else
        retcode = -1;    // fopen err: set retcode=err

    // sale con el retcode seteado
    return retcode;
}
Yo diría que el código es tan intuitivo (y comentado) que no hay mucho más que añadir: se busca el código de referencia (/ *!) Y se copia en un archivo de documentación todo lo que sigue hasta el final del comentario. La función formatLine() (que he omitido) puede no hacer nada (es decir, devolver directamente la línea pasada), o hacer algo de procesamiento más o menos sofisticado (como mínimo recomiendo quitar el inicio de linea como " *"). Yo, para probar la aplicación, he escrito un main() que lee todos los files fuentes de una directory y escribe los resultados en otra directory (y, yo diría que funciona bien).

Lo que se ha presentado es una alternativa rápida a las herramientas del tipo de Doxigen, que están, por supuesto, recomendadas para grandes proyectos (aunque la alternativa artesanal descrita no esta mal).

Termino con un mensaje para los que piensan (como ya se ha dicho aquí) que escribir comentarios (y, aun mas, los headers y las descripciones de funciones) sea una pérdida de tiempo que alarga el tiempo de desarrollo. Hay que tener la mente abierta, pensar a largo plazo: aunque el tiempo de desarrollo aumente un 10% (y documentar no necesita mucho tiempo, sobre todo si lo haces mientras escribes el código), habrá un día en que se recuperará con intereses el tiempo perdido inicialmente, por ejemplo, en la primera ocasión de mantenimiento o modificación (tratad de hacerlo en un código No Comment, y luego me contáis...). Y el tiempo es oro.

Hasta el próximo post.

domingo, 2 de septiembre de 2012

return, o no return, ésa es la pregunta
cómo estructurar el código en C

Él tenía razón, a veces es difícil tomar decisiones existenciales, y hoy yo os voy a proponer una: ¿salida única o múltiple? Bueno, vosotros diríais: "hay cosas más importantes en que pensar". Por supuesto que si, pero aquí se trata de estilo, y como ya se ha señalado aquí, el estilo es importante. Además esta decisión a tomar no es una invención mía, de esto habló mucho antes que yo uno mucho mejor que yo, aunque ya entonces había alguien que no estaba completamente de acuerdo.

Creo que el dilema es claro: estamos hablando de Programación Estructurada y no de tonterías. Y si alguien está pensando "pero la programación estructurada esta superada", o bien "con la OOP el enfoque es completamente diferente", lo paro de inmediato. Aquí se habla del C, el padre (o al menos el tío) de todas los lenguajes modernos, y C es un lenguaje estructurado. Y punto. Y si pensáis que C y Programación Estructurada están superados, mejor que os toméis una pausa para la reflexión ...

¿Y entonces? Bueno, vamos a ver un ejemplo para ver si así nos aclaramos las ideas. Escribimos una función que abre un archivo, extrae la primera línea, procesa la línea de datos y sale. Como siempre, se podría escribir en varios miles de maneras diferentes, pero aislaremos las dos buenas para el ejemplo (bueno, aislamos también una tercera manera, pero solo para cerrar bonito el post). Consideremos en primer lugar el tipo Single-Exit:
int readFile(
    char *path)
{
    char *line = NULL;
    size_t len = 0;
    int retcode;

    // abro el file en path
    FILE *fp;
    if (fp = fopen(path, "r")) {
        // leo el file
        if (getline(&line, &len, fp) != -1) {
            // test contenido file
            if (!strncmp(line, "output=", 7)) {
                // procesa datos
                // ...

                retcode = 0;    // proc. OK: set retcode=OK
            }
            else
                retcode = -3;   // str err: set retcode=err

            free(line);     // libera recursos
        }
        else
            retcode = -2;   // getline err: set retcode=err

        fclose(fp);     // libera recursos
    }
    else
        retcode = -1;   // fopen err: set retcode=err

    // salgo con el retcode seteado
    return retcode;
}
Y ahora pasamos a analizar su perfecto equivalente del tipo multiple-exit, o sea:
int readFile(
    char *path)
{
    char *line = NULL;
    size_t len = 0;

    // abro el file en path
    FILE *fp;
    if ((fp = fopen(path, "r")) == NULL) {
        // salgo con error
        return -1;
    }

    // leo el file
    if (getline(&line, &len, fp) == -1) {
        // libero recursos y salgo con error
        fclose(fp);
        return -2;
    }

    // test contenido file
    if (strncmp(line, "output=", 7)) {
        // libero recursos y salgo con error
        fclose(fp);
        free(line);
        return -3;
    }

    // procesa datos
    // ...

    // proc. OK: libero recursos y salgo con OK
    fclose(fp);
    free(line);
    return 0;
}
¿Cuál es el mejor tipo? Bueno, para mí (y él) es el tipo Single-Exit, pero hay que decir que el partido del Multiple-Exit es grande y está formado (también) por gente muy preparada.

Entonces, ¿dónde está la verdad, tal vez en el medio? No, decir siempre que la verdad está en el medio es la excusa de los indecisos. Yo elijo la solución con el mejor estilo, la más bella y fácil de leer, y, añado, la que te da más control mientras la escribes: con el tipo Single-Exit es más difícil que te olvides de liberar recursos en el momento adecuado (pensar en un programa más complejo, no en el ejemplo). Y también el tipo Single-Exit es más fácil de leer y depurar (probarlo, pero siempre con un caso más complejo).

Se podría hacer una excepción: cuando con el tipo Single-Exit el nivel de anidamiento es demasiado alto, tal vez se podría hacer un híbrido: empezar con los test básicos (tipo "¿el parámetro pasado es nulo?") y sus respectivos return, y, luego, seguir en Single-Exit (pero, si al final el código resultante no os convence, tal vez es mejor si dividáis en dos la función).

Sin embargo, soy menos radical de lo que parece: entre el código que he escrito en mi pasado hay (basta con ver aquí) código de Multiple-Exit o híbrido. El hecho es que hay que ser un poco flexibles: programar (bien) es un arte, y demasiadas restricciones no ayudan a la expresividad.

Como preanunciado cerramos con el tercer tipo: la siguiente función también funciona, y es un equivalente perfecto de las anteriores:
int readFile(
    char *path)
{
    char *line = NULL;
    size_t len = 0;

    // lo hago todo de una vez
    FILE *fp;
    if ((fp = fopen(path, "r")) && (getline(&line, &len, fp) != -1) &&
            !strncmp(line, "output=", 7)) {

        // procesa datos
        // ...

        // datos procesados: libero recursos y salgo con OK
        fclose(fp);
        free(line);
        return 0;
    }
    else {
        // libero recursos y salgo con error
        if (fp)
            fclose(fp);

        if (line)
            free(line);

        return -1;
    }
}
Bueno, si os estáis planeando escribir una función en este modo (tal vez poniendo aun más test en el mismo if) relajaros, tomaros un descanso, leeros un buen libro y, a continuación, intentarlo nuevamente con la cabeza finalmente libre de malos pensamientos: tal vez (espero) que ya no se os ocurrirá de escribirla de esa manera.

Hasta el próximo post.