Saltar al contenido principal

Programación funcional

La programación funcional es un paradigma de la programación en donde las funciones se convierten en las protaginistas del código que escribimos.

Un paradigma de la programación es una forma de pensar en programación. Otros paradigmas de la programación incluyen la programación imperativa (de la que vamos a hablar en un momento) y la programación orientada por objetos. Estos paradigmas no son excluyentes y se pueden mezclar.

Aunque JavaScript no es un lenguaje puramente funcional, sí es posible programar de forma funcional. En JavaScript las funciones tienen las siguientes características:

  • Se pueden asignar a variables.
  • Se pueden pasar como parámetro de otras funciones (callbacks).
  • Se pueden retornar desde otras funciones.

Funciones nombradas vs anónimas

En JavaScript las funciones pueden tener un nombre o ser anónimas:

// esta es una función nombrada
function namedFn() {}

// esta es una función anónima que estamos almacenando en una variable
const anonymousFn = function() {}

Las funciones nombradas son aquellas que tienen un nombre entre la palabra function y los paréntesis (()). Las funciones nombradas se mueven al principio del scope, a esto se le conoce como hoisting.

Las funciones anónimas se utilizan cuando queremos asignarlas a variables y, en algunas ocasiones, para pasarlas como parámetros de otras funciones o retornarlas desde otra función.

Funciones que reciben funciones

Un lenguaje funcional, a diferencia de un lenguaje imperativo, nos permite componer funciones para solucionar diferentes tipos de problemas. A estas funciones también se les conoce como funciones de alto nivel (high order functions).

Por ejemplo, imagina que tenemos el siguiente arreglo:

const arr = [1, 2, 3];

Ahora queremos imprimir todos sus elementos. En programación imperativa lo haríamos de la siguiente forma:

for (let i=0; i < arr.length; i++) {
console.log(arr[i]);
}

En programación funcional utilizaríamos una función llamada forEach que viene incluída por defecto en todos los arreglos de JavaScript:

const sum = arr.forEach(elem => console.log(elem));

Más compacto ¿no?. Por debajo forEach utiliza for para hacer el recorrido de los elementos. No es que la programación funcional reemplace la programación imperativa, la programación funcional está un nivel por encima de la programación imperativa.

Map

Otro método útil que traen todos los arreglos en JavaScript es map, que nos permite transformar cada uno de los elementos de un arreglo. Por ejemplo, imagina que queremos duplicar todos los valores de arr:

const newArr = arr.map(elem => elem * 2);

// newArr sería [2, 4, 6]

Lo interesante de las funciones es que se pueden anidar para crear código muy sutil. Por ejemplo, el siguiente código duplicaría cada elemento del arreglo y después imprimiría cada elemento duplicado.

arr
.map(elem => elem * 2)
.forEach(elem => console.log(elem));

Separé el código en varias líneas para que sea más fácil de leer. Esto es una buena práctica cuando se anidan funciones.

Reduce

Otra operación muy útil con los arreglos es convertirlos en algo completamente diferente, por ejemplo sumar todos los elementos, o crear un objeto a partir de un arreglo. A esto se le conoce en programación como reducirlos.

En JavaScript todos los arreglos tienen un método reduce que se utiliza precisamente para esto. Por ejemplo, si queremos sumar todos los elementos en arr podemos hacer lo siguiente:

const sum = arr.reduce((acc, elem) => acc + elem);
console.log(sum); // 6

reduce recibe un callback (una función) y, opcionalmente, un valor inicial. El callback recibe dos parámetros: un acumulador y un elemento. Lo que retorne el callback se va a utilizar como el acumulador del siguiente elemento. En nuestro caso vamos acumulando la suma.

También podemos utilizar reduce para operaciones un poco más complejas. Por ejemplo para contar cuantas veces se repite cada caracter en una cadena de texto:

const input = "Make it real"
const response = input.split('').reduce((acc, now) => {
acc[now] ? acc[now] ++ : acc[now] = 1;
return acc;
}, {})

console.log(response) // { M: 1, a: 2, k: 1, e: 2, ' ': 2, i: 1, t: 1, r: 1, l: 1 }

El split hace que la cadena se convierta en un arreglo que podemos utilizar para contar las letras (utilizando el reduce). Al reduce le estamos pasando un objeto vacío como valor inicial (línea 5). En cada iteración del reduce verificamos si la letra existe como propiedad del objeto. Si existe, le sumamos 1 a esa propiedad, de lo contrario la creamos y le asignamos el valor 1. Por último retornamos el objeto actualizado.

Funciones que retornan funciones

La capacidad de retornar una función de otra función es una característica fundamental de la programación funcional que nos permite crear soluciones reutilizables a problemas comunes.

Librerías como Lodash, React y Redux, entre muchas otras, hacen amplio uso de esta capacidad.

Veamos cómo crear y utilizar una función que retorne otra función:

function hello(name) {
return function() {
console.log(`Hola ${name}`)
}
}

const helloMaria = hello("Maria")
helloMaria() // "Hola Maria"

// o más compacto
hello("Pedro")() // "Hola Pedro"

La función hello recibe un argumento (un nombre) y retorna una nueva función. En la línea 7 estamos almacenando la función retornada en una variable llamada helloMaria que después invocamos. También es posible evitarnos la asignación a una nueva variable e invocar la función retornada inmediatamente como lo hacemos en la línea 11.

En la práctica es poco probable que necesites crear una función que retorne otra función, es más común que las utilices en librerías o incluso en el mismo lenguaje.

Por ejemplo, el método bind (explicado en la guía Uso del this) retorna otra función que garantiza que la función original siempre se ejecute en el contexto adecuado.

Otro ejemplo es el método before de Lodash que garantiza que una función no se llame más de 1 vez:

// asumiendo que Lodash está incluido
const calculate = _.once(function() {
// realiza un calculo complejo y retorna un resultado
})

calculate()
calculate() // no ejecuta la función nuevamente, retorna el resultado anterior

Aunque la función once ya está implementado en Lodash podríamos implementarla nosotros mismos:

function once(fn) {
let called = false
let result

return function() {
if (!called) {
called = true
result = fn()
}
return result
}
}

Primero definimos dos variables: called que nos va a decir si la función ya se invocó y result en donde vamos a almacenar el resultado de la función. Después retornamos una nueva función que hace todo el trabajo pesado: si la función no ha sido invocada la invoca, cambia called a true y almacena el resultado en result. Por último, en la línea 10, retornamos el resultado.

Funciones puras

Las funciones puras son funciones que cumplen con las dos siguientes condiciones:

  • Retornan el mismo valor si se les pasan los mismos argumentos.
  • No tienen efectos secundarios (como imprimir en la console, escribir a la red o al disco, etc.).

En la práctica no es posible crear una aplicación a partir de funciones puras únicamente, pero es bueno saber qué es y qué no es una función pura.

El principal beneficio de las funciones puras es que nos permiten escribir pruebas unitarias más fácilmente.

Inmutabilidad

Otro principio importante en la programación funcional es el de inmutabilidad, que dice que las variables, objetos y arreglos no se deberían modificar una vez han sido creados. Eso implica utilizar siempre const en el caso de variables y crear un nuevo arreglo u objeto cuando se necesite modificarlos.

En JavaScript todos los strings son inmutables (no pueden ser modificados). Todos los métodos sobre los strings devuelven nuevos strings. Por ejemplo:

const str = "Este string es inmutable"
const str2 = str.slice(5, 11)
const str3 = str2.toUpperCase()

console.log(str) // "Este string es inmutable"
console.log(str2) // "string"
console.log(str3) // "STRING"

Lo más importante de este ejemplo es que str nunca cambia, los métodos slice y toUpperCase retornan nuevos strings.

Los arreglos y objetos, por otro lado, son mutables (se pueden modificar).

Los arreglos tienen algunos métodos que mutan (modifican) el arreglo (p.e. push, shift y splice) y otros que retornan un nuevo arreglo (p.e. concat, map y filter).

Para modificar un arreglo de forma inmutable se recomienda crear un nuevo objeto:

const obj = { a: 1, b: 2, c: 3 }
const newObj = { ...obj, c: 4, d: 5}
// newObj queda { a: 1, b: 2, c: 4, d: 5}

La ventaja principal de la inmutabilidad es que permite crear programas que son más fácil de entender y mantener. En programas pequeños esto no es muy relevante, pero a medida que los programas crecen se vuelve más importante.