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, 24 de enero de 2013

¿Prototipos? Sí, ¡gracias!
cómo usar los Prototipos en C

Tras los excesos de las fiestas (y, tal vez, con un par de kilos acumulados por eliminar), es mejor empezar con un tema ligero ligero: los Prototipos de Funciones. Ligero, pero no demasiado.

Después de una inspección rápida en la red me he dado cuenta de que hay una cierta confusión sobre el tema. Prototipos necesarios, quizás recomendados, a veces desconocidos... Me he dado cuenta de información dudosa hasta en apuntes Universitarios (¡ay, ay!). Por cierto, en mi pasado, he hablado con varios colegas C-Programmers que no tenían ideas claras sobre el tema. Bueno, entonces es el momento de aclarar!

Vamos a empezar con los hechos, dejando para la segunda parte las consideraciones técnicas/filosóficas. Recomiendo prestar atención, en el siguiente texto, en ciertas palabras claves que usamos y vamos a tratar de ilustrar: declaración, prototipo y definición. Y, nos referiremos a las distintas versiones de C que nos han acompañado hasta la fecha, lo que a su vez son: K&R C, ANSI C (C89/C90) y C99 (estaría, también, el C11, pero no es significativo para este post). A menos que se especifique diversamente todos las próximas afirmaciones/consideraciones se refieren al C actual, el C99.

Vamos al grano: en C los prototipos no son obligatorios. La confusión sobre este tema proviene de la doble personalidad que muchos programadores de C (yo incluido) que, a menudo, tiene que desenredarse entre C y C++, haciendo, a veces, un poco de confusión: los prototipos son obligatorios en C++, por razones estrechamente relacionadas con ciertas características del lenguaje (¿os suena el Function Overloading?).

En C, sin embargo, es obligatoria la declaración de una función.

Hagamos, pues, un ejemplo sobre las palabras claves declaración, prototipo y definición, usando sólo un tipo moderno de sintaxis (ANSI C o C99):
// declaración de función
int myFunc();

// declaración de función con prototipo
int myFunc(int val):

// definición de función con prototipo
int myFunc(int val)
{
   if (val > 5)
       return val;
   else
       return val * 2;
}
El orden en este ejemplo, como es evidente, no es casual: la declaración es el caso básico, el prototipo contiene implícitamente una declaración, y, por último, la definición contiene implícitamente un prototipo (y por lo tanto también una declaración). Como se mencionó, en el ejemplo he omitido, para no complicar innecesariamente la descripción, sintaxis permitidas pero demasiado old-fashioned, o prohibidas por el C99.

Antes de trasladarse a la parte filosófica, hacemos un breve análisis histórico: en K&R C no había obligación de declarar las funciones, así que no había ninguna comprobación en compile-time sobre el valor de retorno y, menos aún, sobre la consistencia de los parámetros pasados: en ausencia de la declaración, el compilador aplicaba un comportamiento por defecto asumiendo que la función devolvía un int. Para los parámetros se aplicaba el default argument promotion: los enteros se promoven en int, y los float se promoven en doble.

Con la llegada de ANSI C (o C89/C90), han llegado los prototipos, pero se ha mantenido la compatibilidad hacia atrás con la sintaxis antigua (para no obligar a revisar millones de líneas de código funcionante). Con esta novedad era, finalmente, posible comprobar en compile-time el uso correcto de la funciones, sea para los parámetros sea para los valores devueltos. Debido a la compatibilidad hacia atrás se mantuvo, sin embargo, la posibilidad de escribir nuevo código con la sintaxis antigua, y, además, seguía siendo válido el concepto de default return value en ausencia de declaración.

Con el C99 se ha dado otro paso adelante: vale con la búsqueda de compatibilidad con el código existente, pero el valor de retorno por defecto era un agujero demasiado grande en la solidez del lenguaje, por lo tanto se ha introducido la declaración obligatoria como se indicaba al principio del post (añado que también se hizo obligatorio el uso de prototipos en los standard headers del lenguaje, pero esa es otra historia...).

Y ahora, después de describir lo que el standard nos exige y/o permite hacer, llegamos finalmente a lo que es mejor hacer: Creo que un buen programador utiliza prototipos (de ahí, supongo, para la propiedad transitiva aquellos que no utilizan prototipos no son buenos programadores. He dicho supongo, así que si alguien se ha ofendido, no se lo tome conmigo, se lo tome con la propiedad transitiva). ¿Y por qué recomiendo encarecidamente el uso de prototipos? Bueno, C es un lenguaje tipizado, por lo tanto es tan obvio la ayuda que este mecanismo nos puede dar para producir código sin errores de tipo, al tiempo que mejora la legibilidad y facilidad de mantenimiento, que no hay necesidad de explicarlo!

Y, para añadir un toque de radicalidad que nunca sobra, añado que, para las mismas cuestiones de legibilidad y mantenibilidad del software, no es conveniente contar con el hecho de que usando definiciones con prototipo (véase el ejemplo anterior), y escribiendo el código en el orden correcto (es decir, utilizando una función sólo después de su definición), no es necesario escribir prototipos reales. No seáis perezoso en cosas útiles, por favor!

¿Y como debe de estar estructurado un buen código considerando lo dicho anteriormente? Veamos un pequeño ejemplo con tres archivos: un header, un archivo con las funciones, y un archivo que las utiliza:

Este es el archivo header:
/* myfuncs.h
 */
// prototipos globales
char *myFunc1(char *dest, const char *src);
char *myFunc2(char *dest, const char *src);
Aquí está el archivo con las funciones:
/* myfuncs.c
 */
#include "myfuncs.h"

// myFunc1()
char *myFunc1(char *dest, const char *src)
{
    ...
}

// myFunc2()
char *myFunc2(char *dest, const char *src)
{
    ...
}
Y, finalmente, el archivo utilizador:
/* usefuncs.c
 */
#include "myfuncs.h"

// prototipos locales
static int useFuncs(void);
static int anotherFunc(void);

// anotherFunc()
static int anotherFunc(void)
{
    ...
    int res = useFuncs();
    ...
}

// useFuncs()
static int useFuncs(void)
{
    ...
    char *p1 = myFunc1(dest, src);
    char *p2 = myFunc2(dest, src);
    ...
}
¡Y ya no hace falta decir nada más!

Hasta el próximo post.

lunes, 24 de diciembre de 2012

La maldición de la Callback de jade
cómo escribir una Callback en C

Hoy vamos a hablar de un tema un poco especial, las funciones callback. ¿Porqué especiales? Bueno, para empezar, en la biblia del C (el K&R) nunca se habla del argumento, así que un radical C-ista también podría afirmar que "¡las callback no existen!". En realidad, en el K&R se habla ampliamente de los tíos de las callback, es decir, los punteros a función (de los cuales las callback son un caso especial): entonces las callback existen.

No voy a escribir un tratado sobre el uso de las callback (ya puedo escuchar vuestros suspiros de alivio), ni voy a explicar cómo, cuándo, por qué, y (especialmente) si usarlas: hay muchas fuentes en la red interesantes y bien escritas. Eventualmente, sería bueno escribir un post sobre sus tíos, pero dado el tema muy complicado y el probable malestar estomacal que me vendría escribiéndolo, lo aplazo a una fecha futura (para ser claros: incluso Kernighan y Ritchie hablando de Pointers to Functions han titulado el capítulo 5.12 del K&R "Complicated Declarations", y si eran complicadas para ellos...).

¿De qué vamos a hablar entonces? Bueno, yo (como muchos otros, supongo) he usado muchas veces las callback, y he leído código que las usaba (código que era, en la mayoría de los casos, ilegible y difícil de interpretar en relación directamente proporcional a la frecuencia de utilización de las callback, sigh). Y bien, hasta el día en el que escribí una aplicación completa (o sea he escrito, además de la callback, la función a la que pasarla) no me he dado cuenta de algunos detalles ocultos. Por supuesto, si para vosotros las callback no tienen detalles ocultos, podéis dejar de leer lo siguiente y nos vemos en el próximo post.

¿Todavía estáis aquí? Vale, antes de empezar vamos a ver una definición de las callback tomada textualmente de Wikipedia: "una función que se usa como argumento de otra función", y yo agregaría: "y, con frecuencia y/o normalmente, la función llamante es una función de librería". El ejemplo más clásico y conocido citado en literatura es relativo al uso de qsort():
// funzione callback de comparación para la qsort()
static int cbCmpFunc(const void *elem_a, const void *elem_b)
{
    return *(int*)elem_a > *(int*)elem_b;
}

// main
int main(void)
{
    int array[] = {34,12,32,9,10,72,82,23,14,7,94};
    int nelems = sizeof(array) / sizeof(int);

    // ejecuto sort array con qsort()
    qsort(array, nelems, sizeof(int), cbCmpFunc);

    // enseño resultados
    int i;
    for (i = 0; i < nelems; i++)
        printf("%d - ", array[i]);
    printf("\n");

    // salgo
    return 0;
}
La qsort() es una función de libc que implementa el algoritmo de ordenación quicksort, y requiere una función de callback que especifique el tipo de ordenación deseado. Estamos, por tanto, en un caso clásico: qsort() es una función de libreria, y nosotros, a nivel local, la usamos pasándole una callback que, en nuestro ejemplo, se utiliza para ordenar en orden ascendente los números del array.

Y ahora vamos al grano: en otros tiempos, cuando no era fácil como ahora encontrar documentación y ejemplos, se me ocurría leer y escribir código como el que se muestra arriba y me preguntaba (quizás sólo inconscientemente): "pero de donde salen los parámetros elem_a y elem_b de cbCmpFunc()? " y otra vez: "Si llamo a la callback y no le paso explícitamente parámetros, cómo funciona todo?" Bueno, como descubrí más tarde, yo estaba pensando invirtiendo la relación causa-efecto: no era yo quien llamaba la callaback, ¡era la qsort() que la llamaba! Vale, me avergüenza un poco tener que contar una conclusión tan perogrullada, pero, efectivamente, lo comprendí a fondo solo el día en que necesité escribir una función de librería que utilizaba una callback. Claro, ahora con toda la información en la red es mucho más fácil ...

Así que para aclarar, vemos un ejemplo completo (NOTA: el ejemplo se podría escribir todo en un file, pero, como se muestra en los comentarios apropiados debería ser dividido en tres file en un proyecto real):
/* parte que tendría que estar en el file mysort.h
*/
// prototipos para mySort()
typedef int (*mycallback)(int, int);
void mySort(int *array, int nelems, mycallback cmpFunc);

/* parte que tendría que estar en el file mysort.c
 */
// mySort() - función de sort que usa el algoritmo bubblesort
void mySort(int *array, int nelems, mycallback cmpFunc)
{
    // loop sobre todos los elementos de array
    while (nelems > 0) {
        // loop interno con decremento longitud
        int i;
        for (i = 0; i < (nelems -1); i++) {
            // ejecuto callback de comparación
            if (cmpFunc(array[i], array[i + 1])) {
                // eseguo swap di array[i] e array[i+1]
                int temp = array[i];
                array[i] = array[i + 1];
                array[i + 1] = temp;
            }
        }

        // decremento nelems
        nelems--;
    }
}

/* parte que tendría que estar en el file mymain.c
*/
// cbCmpFunc() - función de comparación
static int cbCmpFunc(int elem_a, int elem_b)
{
    return elem_a > elem_b;
}

// main
int main(void)
{
    int array[] = {34,12,32,9,10,72,82,23,14,7,94};
    int nelems = sizeof(array) / sizeof(int);

    // ejecuto sort array
    mySort(array, nelems, cbCmpFunc);

    // enseño resultados
    int i;
    for (i = 0; i < nelems; i++)
        printf("%d - ", array[i]);
    printf("\n");

    // salgo
    return 0;
}
Como podéis ver es muy similar al ejemplo de la qsort(), pero, en lugar de utilizar una función de la libc, se utiliza una función de ordenación escrita ad-hoc, la mySort(). Para este ejemplo he utilizado, para no complicar demasiado el código, un algoritmo de tipo bubblesort, que es (lo sé) un asco, pero, para hacer una prueba sencilla es mas que suficiente. Como notaréis es la mySort(), la que se encarga de escribir los argumentos para la callback, procesando correctamente los otros parámetros que se pasan (array y nelems), y así, mágicamente, aparecen los valores en elem_a y elem_b.

¿Qué os parece? Sí, un poco ingenuo (tal vez), pero que levante la mano quien nunca tuvo dudas en su historia de programador (uhmm... veo pocas manos levantadas). Y si este post ha servido para ampliar el conocimiento sólo a uno de mis lectores estoy super-feliz. Y para los otros, aquellos que ya lo sabían todo de las callback: porque habéis llegado igualmente al final del post?

Hasta el próximo post.