“Programación líneal” es evitar bifurcaciones condicionales como los if o switch y reemplazarlas por casos únicos. Estos casos únicos suelen contener expresiones más sofísticadas; en cambio se gana en legibilidad y testabilidad del código.

Expresiones booleanas

Como un ejemplo de introducción hablamos de las expresiones booleanas, que suelen ser un caso frecuente de aplicar un estilo “lineal”.

Podemos asignar el valor de una variable booleana medianta una bifurcación if:

bool igual;
if (a == b)
{
    igual = true;
}
else
{
    igual = false;
}

Sin embargo, todo esto podemos escribir en una sólo línea:

bool igual = a == b;

La segunda forma he bautizado “líneal”, porque el programa no tiene saltos. La misma sentencia se ejecuta en todos los casos. Por eso mejora la testabilidad del código. No necesito contemplar dos casos distintos (a igual a b y a desigual a b) para ejecutar todas las líneas del código al menos una vez.

Este ejemplo demuestra también que la programación líneal mejora la legibilidad: Varias líneas de código se acortaron a una sola. Una mejora de legibilidad no se da siempre pero a menudo.

Arriba he mencionado también, que la sustitución de saltos conlleva expresiones más complejas. Esta características observamos también en este ejemplo. La primera forma parte de una expresión “si algo es cierto, entonces haz una cosa, sino haz otra”. Esto es más intuitivo que la segunda forma, que es una ecuación booleana. Un cálculo booleano no suele presentar demasiada dificultad para un programador veterano, pero en casos más complejos puede acabar en expresiones menos intuitivas.

Linealizar con el operador ?:

Las asignaciones condicionales se puede casi siempre escribir en una sentencia. Una expresión condicional es del tipo

if (algo)
{
    mi_variable = función_de_algo;
}
else
{
    mi_variable = función_de_no_algo;
}

Por ejemplo, si quiero acotar un rango, entonces el algo sería una expresión como a < max_a. Si estoy dentro del rango, la función_de_algo sería la variable a, si estoy fuera sería max_a.

if (a < max_a)
{
    mi_variable = a;
}
else
{
    mi_variable = max_a;
}

La forma más directa de reemplazar el if sería con el operador “?:”. No todos los lenguajes ofrecen este operador, pero muchos.

mi_variable = a < max_a ? a : max_a;

Esta expresión podemos hacer todavía un poco más legible. Acotar el valor máximo de un rango corresponde a tomar el valor mínimo del valor a acotar y el límite. Por lo tanto podemos escribir el ejemplo arriba como

mi_variable = min(a, max_a);

Por supuesto, el operador ?: no ha desaparecido. Se encuentra ahora en la función min. Sin embargo, hemos ganado en testabilidad. Podemos comprobar el funcionamiento de la función min por separado y hemos simplificado la expresión que asignamos a mi_variable.

Crítica al operador ?:

Un buen observador habrá notado que realmente no hemos reemplazado el if de todo. Ya no aparece la palabra clave if, pero el operador ?: es una especie de notación abreviada de un if. El hilo de la ejecución pasa por la expresión en todos los casos, pero no ejecuta toda la expresión. Hemos mejorado la legibilidad, pero no testabilidad.

Una primera manera de trater este problema ya hemos introducido: esconder el operador ?: en una función. Entonces todavía nos queda comprobar dos casos distintos en esta nueva función, pero suele ser bastante más simple comprobar una función como min que una expresión con el operador ?: en un contexto complejo donde, además, puede haber varias bifurcaciones.

Debemos tener en cuenta que cada if multiplica el número de posibles casos por dos. Dividir los if en funciones pequeñas simplifica las pruebas. Una función con tres bifuraciones tendría 23 = 8 casos distintos. Reemplazar estos if por funciones pequeñas nos dejaría dos casos por función – que serían 2 · 3 = 6 casos.

Debemos tener en cuenta también, que el operador ?: dificulta la depuración, por lo cual sólo debería emplearse para casos simples. Al contrario de una estructura if, no tenemos un bloque de if y de else, donde podemos colocar un punto de interupción. Además, el operador ?: sólo puede eligir entre expresiones. Si queremos ejecutar sentencias de forma condicional, no lo podemos usar. Debemos usar el if.

Expresiones incondicionales

Hay casos en que podemos prescindir del operador ?: por completo y podemos usar una expresión incondicional, que sería una expresión que básicamente usa operaciones aritméticas como más y menos.

Para ello presentamos un ejemplo más complejo. Imaginemos que tenemos una tabla de base de datos y queremos imprimir los datos válidos. Una columna VALIDO contiene un uno si el registro es válido y cero en el caso contrario. El tratamiento habitual sería hacer un bucle con un if anidado. Si la columna VALIDO es distinta de cero, añadimos el registro a la salida final.

for (Registro registro : todos_los_registros) 
{
    if (registro.valido == 1)
    {
        String salida = registro.toString();
        print(salida);
    }
}

Ahora reemplazamos el if por una expresión incondicional. En su lugar aparece una expresión más compleja con una multiplicación.

for (Registro registro : todos_los_registros) 
{
    String salida = registro.toString();
    print(salida.substring(0, salida.length() * registro.valido));
}

Si el registro es vállido, entonces registro.valido vale uno y la multiplicación no altera el valor de salida.length(). La expresión

salida.substring(0, salida.length() * 1))

es igual a salida. En el caso contrario de que registro.valido vale cero, el producto salida.length() * registro.valido vale cero y llegamos a la expresión salida.substring(0, 0) – que es un string vacío. En este caso se imprime un string vacío como print("") que realmente no imprime nada.

Tenemos el mismo comportamiento que en la versión con la condición if pero sin usar una bifuración. Al mismo tiempo hemos hallado un ejemplo de como la expresión resultante se puede complicar. No es muy intuitivo multiplicar la longitud de una cadena de texto con un valor en general y mucho menos con el fin de suprimir esta cadena.

Reemplazando los saltos llegamos a menudo a una situación donde debemos decidir si prevalece la testibilidad del código o la intuición del lector del programa. Como criterio podemos usar la legibilidad del código. Si podemos reemplazar muchas líneas por pocas bien comentadas solemos ganar en general. Sin embargo, siempre debemos comprobar si no ganamos aún mucho más si pasamos el cuerpo de un bucle a una función.

Trabajar con datos erróneos

Muchas veces hace falta comprobar la validez de datos antes de usarlo. En general conviene comprobar los datos donde se usan. Es decir, una función que sólo pasa un dato como parámetro a otra función, no debería comprobar el dato en general. No obstante, algunas bibliotecas como la biblioteca estándar de C no comprueban la validez de los datos y obligan al usuario de sus funciones de comprobar la validez de datos. Una función de usuario, en cambio, debería devolver un código de error o lanzar una excepción cuando no puede seguir por un dato erróneo.

La comprobación de errores conlleva a menudo un if: “Si esto es válido, entonces hazlo.” Sin embargo, hay ocasiones en que uno puede utilizar un dato malo. Esto devolverá un resultado malo, pero puede simplificar la comprobación de errores. Por ejemplo, no hace falta comprobar la validez de una ruta en el sistema de ficheros. Si la función fopen no la encuentra, entonces devuelve un controlador nulo. Una comprobación de este controlador sería suficiente para protegerme contra cualquier error de fichero – no sólo contra una ruta de fichero errónea.

En programas que hacen un uso exhaustivo de excepciones muchas veces no hace falta ningún if. Yo puedo calcular el índice en un array sin miedo, ya que se pueden hacer todas las operaciones aritméticas con números enteros aunque los números no representen algo físico. Si el índice calculado luego no existe en el array, ya se lanzará una excepción.

Este enfoque tiende a un tiempo de cálculo menor cuando los datos son buenos, ya que se hacen menos comprobaciones. En cambio incrementa el tiempo de cálculo con datos malos, ya que se realizan cálculos que luego no tienen un resultado útil. Si consideramos un dato malo un caso excepcional, entonces ahorramos más que perdemos con cada comprobación de que podemos prescindir.

Conclusión

Hemos visto como la “programación líneal” simplifica el código y mejora su testabilidad. En general mejora también la legibilidad aunque tiene tendencia a introducir expresiones más complejas que a veces no son intuitivas y requieren un buen comentario explicativo.

Se puede conseguir un resultado optimal utilizando varias técnicas: usando el operador ?:, empleando a funciones auxiliares y finalmente comprobando la validez de los datos donde se usan. En todo caso no hay un camino de oro para la “programación líneal”. Hay que averiguar si prevalece la belleza del código o su intuitividad: un empleo de muchos if puede ser la mejor representación de la descripción humana de un algoritmo.

Lectura adicional

Anuncios