Es inevitable recaer en su uso y no sólo son parte fundamental del lenguaje sino que lo definen. Cómo hemos visto, son parte indispensable en la creación de objetos, bien haciendo uso de herencia o composición. Pero sólo es la punta del iceberg.

Cómo vimos en el capítulo Javascript #2, el uso de funciones convierte a Javascript en un lenguaje multiparadigma haciendo que podamos recurrir a la programación funcional, a la recursividad y a la orientación a eventos (programación asíncrona).

De momento solo voy a pararme en las dos primeras y ya veremos más adelante la programación asíncrona cuando hagamos uso de eventos.

Programación funcional

Como ya dije en el post Javascript #2, podríamos resumir su definición cómo "el uso de funciones que devuelven valores en lugar del almacenamiento y mutación de los mismos".

La programación funcional tiene su origen el cálculo lambda, un lenguaje matemático con el que se puede expresar y evaluar cualquier función que sea computable.

Dicho de otra manera, cualquier valor puede ser expresado como el resultado de una función, y por tanto cualquier operación puede ser expresada como un conjunto de funciones. Un ejemplo sencillo, la suma de dos números.

const total = 2 + 3;  
console.log(total);       // 5

const sum = (a, b) => {  
  return a + b;
};
console.log( sum(2, 3) ); // 5  

Si combinamos distintas funciones podemos conseguir operaciones mas complejas anidándolas tantas veces como queramos.

const multiply = (a, b) => {  
  return a * b;
};

console.log(  
  multiply(
    sum(3, 5),
    sum( multiply(3, 2), 3)
  )
); // 72

Sin embargo también dijimos que en Javascript las funciones son objetos que pueden pasarse como parámetros a otras funciones, es decir, podríamos pasar la función como parámetro sin tener que ejecutarla antes. Pero también, como vimos en el capítulo Javascript #6, es posible que una función devuelva otra función.

Funciones de orden superior

Definimos como función de orden superior a aquellas funciones que requieren como argumentos una o varias funciones; o que devuelven otra función como resultado. En Javascript tenemos muchas funciones de este tipo ya incluídas en la definición del lenguaje, aunque generalmente las más usadas son las que provee el objeto Array.prototype en el que delegan los arrays y que son las que usaré en este artículo.

Veamos un ejemplo para entender como funcionan. Tenemos una lista de números de los que queremos obtener el cuadrado de cada uno. Para ello usaremos la función de orden superior map. Esta función acepta como argumento otra función, que se encargará de producir la operación que queremos conseguir, y a la que llamará usando como argumento cada elemento del array para producir un nuevo array.

const numbers = [1, 2, 3, 4, 5, 6, 7, 8];  
const squares = numbers.map(num => num*num);

console.log(squares); // [ 1, 4, 9, 16, 25, 36, 49, 64 ]  

Como nos interesaba realizar esta operación solo una vez le hemos pasado la función anónima num => num*num (usando las fat arrow de ES6). Pero puede interesarnos guardar esta operación para usarla más veces. Podemos asignarla a una variable y pasarla como argumento o usarla directamente.

const square = (num) => num * num;  
console.log( numbers.map(square) ); // [ 1, 4, 9, 16, 25, 36, 49, 64 ]  
console.log( square(9) ); // 81  

Si en lugar de recurrir a la programación funcional quisiéramos lograr el mismo resultado de manera imperativa usando bucles tendríamos que hacer lo siguiente.

let squares = [];  
for (let i=0, j=numbers.length; i<j; i++) {  
  let num = numbers[i];
  squares.push(num * num);
}
console.log(squares); // [ 1, 4, 9, 16, 25, 36, 49, 64 ]  

No sólo tenemos que escribir mucho más sino que quien lea nuestro código necesita más tiempo para comprender qué es lo que hace.

Vamos a ver otro ejemplo usando la función forEach. Esta función realiza la operación que le digamos en cada elemento de un array pero sin generar uno nuevo. Resulta útil si solo queremos iterar sobre los elementos con algún fin. Por ejemplo vamos a imprimirlos en consola uno a uno.

numbers.forEach(num => console.log(num));  
// 1
// 2
// ...

¿Y si queremos imprimir uno a uno los cuadrados de una lista? Sabemos que map devuelve un array y por tanto podemos encadenar operaciones.

numbers.map(square).forEach(num => console.log(num));  
// 1
// 4
// 9
// ...

Compliquémoslo aún más. Queremos obtener la suma de los cuadrados de los números que sean pares, y además conservar nuestras funciones para usarlas más veces.

Para obtener la suma usaremos reduce. Esta función recorre los elementos de un array y ejecuta la operación que le digamos sobre un objeto acumulador. Además acepta de forma opcional como segundo parámetro el objeto inicial que queremos usar como acumulador, si no usará los dos primeros elementos como argumentos en la primera iteración (cómo acumulador y elemento reespecticamente).

Para el filtrado de los números pares usaremos filter. En este caso la función deberá devolver un valor booleano en base a si queremos filtrar el elemento. Los elementos que devuelvan true serán devueltos en un nuevo array. Vamos por tanto a definir una función reductora para la suma y otra para el filtrado.

const sum = (a, b) => a + b;  
const even = (num) => num % 2;  
numbers  
  .filter(even)
  .map(square)
  .reduce(sum); // 84

Para filter he usado la operación módulo, que devolverá 0 o 1 en este caso y que se interpretará como un booleano. Cualquier número distinto de 0 es interpretado como true (para más información consultad que se valores se evalúan como falsy y truthy)

Si queréis conocer mas funciones para trabajar sobre arrays podéis mirar la documentación de Array.prototype.

Pero antes dijiste que las funciones de orden superior también pueden devolver otras funciones. Exacto, vimos que con funciones de orden superior se pueden crear closures por ejemplo. Pero tambien podemos usarlas para, por ejemplo, componer funciones.

Supongamos por ejemplo que queremos realizar dos operaciones, el cuadrado y el doble de un número, sobre un array para devolver uno nuevo usando map, y luego sumar todos los elementos usando reduce. En lugar de encadenar varias funciones vamos usar una función que lo haga por nosotros en un solo paso.

const compose = (reducer, func1, func2) => {  
  return (acumulator, elem) => {
    return reducer(
      func1( func2(elem) ),
      acumulator
    );
  }
};

const square = num => num * num;  
const double = num => num * 2;  
const sum = (a, b) => a + b;

const dobleSquareSum = compose(sum, square, double);  
numbers.reduce(dobleSquareSum); // 816  
[1, 2, 3].reduce(dobleSquareSum); // 53

Obtenemos una función capaz de realizar en un solo paso todo lo que necesitamos y además conservando las funciones originales. Más adelante comentaré otras formas de componer funciones haciendo uso de métodos más avanzados.

Funciones recursivas

Podemos describir la recursividad como la capacidad que tiene una función para llamarse a sí misma y lograr un resultado. Para ello necesitamos antes tener claras dos cosas: qué operación vamos a realizar y cuál es la condición de retorno para escapar del bucle. Podríamos pensar en un bucle while como si fuera un análogo de una función recursiva, solo que la función permite ramificar su ejecución dentro de un árbol.

Copiaré de nuevo el ejemplo que puse en el capítulo Javascript #2 para calcular el factorial de un número (pero usando fat arrows).

const factorial = (n) => {  
  if (n > 1) {
    return n * factorial(n - 1);
  } else {
    return 1;
  }
}

factorial(4); // 24  

La operación será la multiplicación de dos números (n y n-1) y usaremos como condición de retorno que el número sea superior a 1. Podemos simplicarla más aún usando operadores ternarios.

const factorial = n => (n > 1) ? (n * factorial(n - 1)) : 1;  
factorial(4); // 24  

Los operadores ternarios son una forma de simplificar un if-else y devolver el resultado de la condición.

(condición) ? (valor verdadero) : (valor falso)

Descrita de otra forma la podríamos traducir como lo siguiente.

factorial(4) -> (4 * (3 * (2 * (1))))  

La función produce un bucle recursivo en el que genera todas las posibilidades hasta que encuentra la condición de retorno. Es entonces cuando el bucle vuelve sobre si mismo realizando la operación que le hemos dicho.

Qué original... ¿Qué es lo próximo? ¿Un Fibonacci? No quiero caer en ejemplos típicos así que propongamos algo mas complicado y real para ver como de pontentes son las funciones recursivas.

Supongamos que tenemos una lista de mensajes de usuarios. Cada mensaje tiene un ID, un texto y opcionalmente el ID de otro mensaje al que hace referencia (una respuesta). Tendríamos algo así.

const messages = [  
  [1, "Hi, I'm John"],
  [2, "Javascript FTW"],
  [3, "Hi John!", 1],
  [4, "JS rulz", 2],
  [5, "Yep!", 2],
  [6, "ES6 it's better", 4],
];

Queremos producir un objeto que tenga los mensajes de forma anidada para visualizarlos mejor, vamos a genera una función que lo haga por nosotros. Además vamos a mezclarla con la función filter para filtrar las respuestas y con la función forEach para recorrer las respuestas encontradas.

const tree = (messages, parent) => {  
  let node = {};
  messages
    .filter(msg => msg[2] === parent)
    .forEach(msg => {
      node[msg[0]] = { 
        text: msg[1], 
        replies: tree(messages, msg[0]) 
      };
    });
  return node;
};

Esta vez usaremos también JSON.stringify para convertir primero el resultado ya que únicamente con console.log no vamos a poder mostrar mas de 3 anidaciones.

console.log(  
  JSON.stringify(tree(messages), null, 2)
);

/* 
{
  "1": {
    "text": "Hi, I'm John",
    "replies": {
      "3": {
        "text": "Hi John!",
        "replies": {}
      }
    }
  },
  "2": {
    "text": "Javascript FTW",
    "replies": {
      "4": {
        "text": "JS rulz",
        "replies": {
          "6": {
            "text": "ES6 it's better",
            "replies": {}
          }
        }
      },
      "5": {
        "text": "Yep!",
        "replies": {}
      }
    }
  }
}
*/

Pensad ahora cuánto código habría que escribir para conseguir lo mismo de forma imperativa. Hacerlo de manera recursiva no sólo es más directo y limpio sino más eficiente.

En el próximo capítulo veremos la segunda parte sobre funciones donde explicaré otra de las ventajas de que las funciones se comporten también como objetos y otra forma de componerlas.

"Hola, soy el Señor Lobo. Soluciono problemas." - Pulp Fuction (1994)