Fun with functions: La función escalón

Hoy pongo a descansar la desestructuración por un rato y voy a hablar de una técnica que permite evitar plagar nuestro código con condicionales y bucles, que en general introducen ruido en el mismo, obligándonos a pensar más en el como que en el qué queremos hacer.

Supongamos que estás haciendo un programa que calcula la media de varias notas.

Supongamos que para poder hacer media todas las notas tienen que cumplir la condición de ser mayores de 5. Si alguna es menor el suspenso es automático.

Puestos a suponer, supongamos que no queremos usar condicionales.

Para conseguir el objetivo que nos hemos marcado como reto (que una vez más debo agradecer a Jaime), déjame que de un pequeño rodeo. En matemáticas tenemos una función peculiar que se llama función escalón:

El valor de retorno depende del parámetro t. Si el valor de t es menor que el valor de corte a el valor que devuelve la función es 0, y si t es mayor que a el valor que de devuelve es 1.

Esta función es muy útil en análisis matemático y si somos capaces de definirla en Javascript nos va a permitir conseguir el objetivo de este reto particular de una forma muy elegante; recuerda que no queremos usar condicionales:

function escalon(x, corte) {
    return !!Math.max(x-corte, 0) * 1;
}

Analicemos por partes:

  • Math.max(x-corte, 0) devolverá 0 si x es menor o igual que corte y un valor mayor que cero (x - corte) en caso contrario.
  • El operador doble negación !! dispara la coerción automática en Javascript y convierte el valor devuelto en el paso anterior a booleano, el resultado será true si es mayor que 0, y false si es 0.
  • Al multiplicar el valor anterior por un número, en este caso 1 volvemos a disparar la coerción automática, convirtiendolo en 1 si era true o en 0 si era false . Si prefieres ser más explícito, puedes escribir: return Number(Boolean(Math.max(x-corte, 0)))
  • O sea que la función devolverá 0 si x es menor o igual que corte o 1 si x es mayor que corte.

¿Retorcido? Puede. Pero hemos abstraído todas estas complejas operaciones de coerción de tipos en una función con un nombre descriptivo fácil de entender, que nos va a permitir prescindir de los condicionales en el caso que nos ocupa, y escribir la solución de forma declarativa en lugar de imperativa.

Ahora, así es como la usamos:

var notas = [9, 7.5, 5, 3, 4];

var todoAprobado = notas.reduce( 
    (mult, act) => mult * escalon(act, 4.99), 1 );

var media = notas.reduce( 
    (acum, act) => acum + act, 0 ) / notas.length;

var notaFinal =  todoAprobado * media; 

Si en el array de notas hay uno o más valores que sea menor o igual que 4.99 (nuestra nota de corte), todoAprobado será 0, y si no será 1.

Si no queremos ser tan drásticos y no poner un cero por suspender solo alguno de los exámenes o trabajos, podemos hacer que en ese caso la nota media se reduzca a la mitad; suspendiendo en cualquier caso, pero reflejando el esfuerzo realizado en la cercanía al aprobado:

var notas = [9, 7.5, 5, 3, 4];

var todoAprobado = notas.reduce( 
    (mult, act) => mult * escalon(act, 4.99), 1 );

var media = notas.reduce( 
    (acum, act) => acum + act, 0 ) / notas.length;

var notaFinal =  todoAprobado * media + !todoAprobado * media / 2; 

De nuevo, nos estamos aprovechando de la coerción automática que hace Javascript en el segundo sumando de notaFinal, primero a boolean para aplicarle el operador negación y luego de nuevo a number para hacer la multiplicación con media / 2.

Si no te gusta dejar a Javascript esta coerción o puedes sustituir esa expresión por una más explícita: Number(!Boolean(todoAprobado)) * media / 2. Sin embargo, creo que merece la pena conocer bien las reglas de coerción automática y aprovecharse de ellas en estos casos.

Si esta forma de evitar condicionales te parece enrevesada compárala con la solución tradicional al problema usando condicionales:

var notas = [9, 7.5, 5, 3, 4];
var todoAprobado = true;
var media;

notas.forEach( function(nota) {
    if (nota < 5) todoAprobado = false;
    media += nota / notas.length;
});

var notaFinal;
if (todoAprobado) {
    notaFinal = media;
} else {
    notaFinal = media / 2;
}

Es posible que te parezca más fácil de entender esta segunda forma, pero seguramente sea porque no estás acostumbrado a usar el paradigma funcional de programación.

Una vez que dominas el uso de reduce te acaba pareciendo mucho más declarativa y legible la opción sin condicionales.

Y ya por rizar el rizo, supongamos que queremos hacer una media ponderada de las notas:

var notas = [
    { valor: 9, peso: 0.1 }, 
    { valor: 7.5, peso: 0.2 },
    { valor: 5, peso: 0.5}, 
    { valor: 3, peso: 0.1},
    { valor: 4, peso: 0.1}
];

var todoAprobado = notas.reduce( 
    (mult, act) => mult * escalon(act.valor, 4.99), 1 );

var media = notas.map( nota => nota.valor * nota.peso )
    .reduce( (acum, act) => acum + act, 0 );

var notaFinal =  todoAprobado * media + !todoAprobado * media / 2; 

Por cierto, este es uno de los pocos casos en los que creo que la sintaxis flecha aporta algo de legibilidad al código. Y admito que incluso aquí es debatible si es más legible o no. No es un tema de que sea menos código, sino de qué requiere más esfuerzo mental, entender el código anterior o este otro:

var notas = [
    { valor: 9, peso: 0.1 }, 
    { valor: 7.5, peso: 0.2 },
    { valor: 5, peso: 0.5}, 
    { valor: 3, peso: 0.1},
    { valor: 4, peso: 0.1}
];

var todoAprobado = notas.reduce( function(mult, act) {
        return mult * escalon(act.valor, 4.99);
    }, 1 );

var media = notas.map( function(nota) {
        return nota.valor * nota.peso;
     })
    .reduce( function(acum, act) {
        return acum + act;
    }, 0 );

var notaFinal =  todoAprobado * media + !todoAprobado * media / 2; 

Soy consciente de que las funciones map y reduce son un poco marcianas si no estás acostumbrada a ellas, así que este será uno de los temas que trataré en breve.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.