Programar sería horrible sin el azúcar sintáctico (I)

Seguramente la primera duda que os surgirá a muchos de vosotros es: ¿qué es esto del azúcar sintáctico? El término fue acuñado en 1964 por Albert J. Landin y se refiere a todas esas construcciones que no aportan nueva funcionalidad a un lenguaje, pero que permiten que sea más fácilmente utilizable por seres humanos.

Personalmente, si de mí dependiera no se llamaría azúcar sintáctico, sino bicarbonato sintáctico, ya que no me queda claro que un código fuente pueda ser dulce (salvo que le pongas a las variables nombres de cachorritos, o algo así), pero sí que se puede hacer más digerible su lectura. El ejemplo más habitual de azúcar sintáctico es la notación matricial v[i] que hace más fácilmente interpretable la ya existente *(v + i).

El uso de azúcar sintáctico puede venir fomentado por el propio lenguaje, al incluir construcciones redundantes más simples que aquellas a las que sustituyen, como el claro ejemplo del pre-incremento (++i) y post-incremento (i++) en lugar de la instrucción i = i + 1 utilizada en la sentencia anterior o posterior al uso de la propia variable para otro cometido.

Pero también se puede considerar azúcar sintáctico el código modificado a propósito por un programador de tal manera que aumente su legibilidad sin perder eficiencia. En este caso, independientemente de que el lenguaje provea o no de mecanismos para simplificar la escritura de código, una elección inteligente de las sentencias y estructuras puede ayudar a conseguir ese ansiado mantra del código autodocumentado.

Ejemplos de azúcar sintáctico

No hay un patrón común sobre qué es y qué no es azúcar sintáctico, sino que el sentido común es quien nos dicta qué sintaxis son inteligibles. Algunos autores incluso consideran que la notación aritmética a + b es el azúcar sintáctico de la función a.add(b).

En cualquier caso, podamos denominarlo o no con este término, sí que hay ciertas normas que podemos seguir para intentar conseguir un código más agradable a la vista.

Reduce las sentencias innecesariamente largas...

Si la funcionalidad del código es suficientemente clara, al reducirlo a un menor número de sentencias podemos tener una mayor visión global, en lugar de estar interpretando que toda una porción de código realiza una única función. Por ejemplo, el operador ternario:

int maximo = (a > b) ? a : b;

Con un sólo golpe de vista ya sabremos que esa línea halla el máximo entre dos valores dados, y podemos centrarnos en el resto del código. Sin operador ternario tendríamos que mirar cada parte del condicional, lo que ralentizaría nuestra comprensión global:

int maximo;
if (a > b)
    maximo = a;
else
    maximo = b;

...pero sin pasarte

Hay gente que parece tomarse como un reto personal lo de poder realizar la operación más compleja posible en una sola línea. Aparte del riesgo de quedar como un pedante, también existe el peligro de enmascarar errores difíciles de encontrar.

Por ejemplo, el uso del post-incremento es muy útil, pero no es demasiado aconsejable utilizarlo dentro de un condicional, como se puede ver en esta operación utilizada dentro de un bucle:

maximo = (maximo > v[i++]) ? maximo : v[i];

Por intentar ganar espacio, perdemos claridad. Porque, ¿cuándo se ejecuta realmente ese incremento del iterador del vector? ¿Después de la comparación, o después de la asignación? Coged a 3 ó 4 programadores al azar y veréis que no hay unanimidad en el parecer, siempre habrá alguno que esté equivocado, y eso significa que la probabilidad de introducir errores aumenta.

¿Y si lo sacamos del condicional para estar seguros de que no se incrementa justo después de comprobar la condición?

maximo = (maximo > v[i]) ? maximo : v[i++];

De nuevo corremos el riesgo de no tener claro si ese post-incremento se llegará a evaluar en el caso de que el resultado de la condición sea verdadero.

El sentido común nos dice que es mejor llevarnos el incremento a una sentencia distinta, o incluso que prescindamos del operador ternario.

// Con operador ternario
maximo = (maximo > v[i]) ? maximo : v[i];
i++;

// Con bloque if
if (maximo > v[i])
    i++;
else
    maximo = v[i++];

Evita llenar el código de variables auxiliares...

Seguro que todos habéis leído alguna vez una función escrita por otro programador en la que la primera línea ya te invita a pensar que el funcionamiento es complejo:

var aux, aux2, aux3, temp, temp2;

Las variables auxiliares son necesarias en muchos casos, pero abusar de ellas hace que nuestra atención se distraiga de aquellas con verdadero significado. Uno de los grandes desconocidos que resultan de gran ayuda para clarificar el código es la asignación paralela presente en algunos lenguajes:

a, b, c = x, y, z;

// La asignación anterior es igual a:
a = x;
b = y;
c = z;

Esta asignación paralela puede ser de gran utilidad a la hora de invertir los valores de dos variables, sin necesidad de utilizar la típica variable swap

a, b = b, a;

// La asignación anterior es igual a:
var swap;
swap = a;
a = b;
b = swap;

... pero sin recurrir a prácticas esotéricas

La asignación paralela no existe en todos los lenguajes, pero hay otro medio para intercambiar el valor de dos variables sin necesidad de usar auxiliares. Consiste en realizar tres operaciones XOR (a nivel de bit) consecutivas que, tal y como se puede demostrar matemáticamente, acaban produciendo el deseado intercambio.

¿Qué ventajas supone este método con respecto al uso de una variable swap? De acuerdo, nos ahorramos el espacio en memoria reservado para esa variable, pero a cambio tenemos el mismo número de sentencias y una sintaxis menos clara, con la que pocos sabrían interpretar que se está realizando una inversión de valores.

Sobrecarga los operadores más habituales de tus clases complejas

Cuando tenemos una clase compleja en la que sea fácilmente identificable un atributo sobre el que poder realizar operaciones, la sobrecarga de operadores puede ayudar a clarificar en gran medida el uso que se haga de estos objetos.

Por ejemplo, en una clase que identifique a una cuenta corriente, y que tendrá muchos atributos como el IBAN, el titular, el tipo de interés, etc., podría ser de gran utilidad tener operadores que permitan modificar directamente el saldo.

CuentaCorriente& operator+=(const float abono)
{
    this.saldo += abono;
    return *this;
}

CuentaCorriente& operator-=(const float adeudo)
{
    this.saldo -= adeudo;
    return *this;
}

void transferencia(CuentaCorriente& ordenante, CuentaCorriente& receptor, float importe)
{
    ordenante -= importe;
    receptor += importe;
}

Usa propiedades en lugar de métodos set/get

En lenguajes como C#, el uso de propiedades hace que el código sea mucho más limpio, ya que tras el aspecto de un acceso a un atributo de una clase, en realidad se está llamando a un método set o get que puede contener validaciones u otras operaciones necesarias.

class Color
{
    private uint rojo;
    private uint verde;
    private uint azul;
 
    // propiedad para el canal rojo
    public uint Rojo
    {  
        get
        {
            return this.rojo;
        }
        set 
        {
            this.rojo = (value > 255) ? 255 : value;
        }
    }
}

Teniendo definidas las propiedades es mucho más sencillo realizar una operación como, por ejemplo, aumentar en 10 puntos la intensidad del canal rojo dentro de nuestro color:

// Accediendo con propiedades
color.Rojo += 10;

// Con métodos set y get
color.setRojo(color.getRojo() + 10);

Utiliza el tipo de bucle que mejor exprese la función a realizar

Todos los bucles son en esencia iguales: comprobar condición de parada, iterar, cambiar (o no) una variable, volver a comprobar sobre ella la condición de parada, iterar...

La inclusión de más de un tipo de bucle en un lenguaje concreto nos ayuda a plantear mejor la solución, mientras que dejamos al compilador la tarea de traducir dos bucles aparentemente distintos a una misma secuencia de saltos condicionales en ensamblador.

// Buscar si un valor está incluido en un vector, con un for
for(int i = 0; i < sizeof(v); i++)
{
    if (v[i] == VALOR_BUSCADO)
    {
        encontrado = true;
        break;
    }
}

// Misma operación con un do..while
int i = 0;
do
{
    if (v[i++] == VALOR_BUSCADO)
        encontrado = true;
} while (!encontrado && i < sizeof(v));

// Y con un while
int i = 0;
while (!encontrado && i < sizeof(v))
{
     if (v[i++] == VALOR_BUSCADO)
        encontrado = true;
}

Aunque los tres dan el mismo resultado, se nos ofrecen tres sintaxis distintas para que podamos escoger la que se adecue más a nuestra forma de pensar. Si al plantear el problema piensas "mientras no lo hayamos encontrado...", seguramente optarás por el while, mientras que si tu planteamiento es "recorremos todo el vector y...", tu opción seguramente sea el for. Lo importante, más allá de usar o no breaks, es que en la primera línea del bucle ya se intuyan nuestras intenciones, de modo que quien lo lea lo haga con la misma aproximación con la que nosotros lo planteamos.

Imaginad que sólo existiese el bucle for. Las operaciones con listas y vectores serían fáciles, pero los bucles infinitos (por ejemplo, un bucle de espera de eventos) serían bastante antinaturales.

Los parientes del azúcar sintáctico

Un par de términos muy relacionados con el azúcar sintáctico son la sacarina sintáctica y sirope sintáctico. La metáfora sería que estas prácticas endulzan, pero no mucho, y se suele aplicar a los cambios de sintaxis que al autor le resultan más agradables, pero que no suponen ninguna mejora real para la legibilidad del código por terceros.

Y, por supuesto, no nos podemos olvidar del termino casi opuesto: la sal sintáctica, mucho más interesante que los dos anteriores. Pero de ella hablaremos en la próxima entrega.

Referencia | Syntactic sugar en The new hacker's dictionary
Imagen | Wikimedia 2011 Hackathon

Ver todos los comentarios en https://www.genbeta.com

VER 0 Comentario

Portada de Genbeta