Software Crafters® 2025 | Creado con 🖤 para elevar el nivel de la conversación sobre programación en español | Legal
La programación funcional es una forma de entender nuestro software como una serie de funciones matemáticas en las que, dadas unas entradas determinadas, siempre obtenemos las mismas salidas.
Aunque esté "de moda", este paradigma existe desde antes que la orientación a objetos, y está basado en el cálculo lambda. Sin embargo, hasta hace relativamente poco permanecía en el ámbito académico y científico.
En el caso de JavaScript, aunque no es un lenguaje funcional puro (ya que implementa características imperativas), su flexibilidad y dinamismo nos permiten adoptar un enfoque funcional y métodos como map, filter y reduce nos son de gran ayuda.
Los beneficios de la FP sobre nuestro código son:
Los argumentos son los valores con los que llamamos a las funciones, mientras que los parámetros son las variables nombradas que reciben estos valores dentro de nuestra función:
const double = x => x * 2; // x es el parámetro de nuestra función double(2); // 2 es el argumento con el que llamamos a nuestra función
La aridad de una función hace referencia al número de parámetros que tiene. Así pues, una función de aridad 1 (o unaria) tiene 1 parámetro, una unidad de aridad 2 (o binaria) tiene 2 parámetros y así sucesivamente (por cierto, a la de aridad 3 se le llama ternaria).
En JavaScript, es posible llamar a una función con más argumentos de los parámetros que soporta. Simplemente serán ignorados.
const double = x => x * 2; console.log(double(2, 5)); // double(2); // 4;
Una función es pura cuando:
En el caso de JavaScript, esto tiene especial importancia, porque tanto los arrays como los objetos (también las funciones) se copian por referencia y no por valor. Veamos un ejemplo de un efecto colateral.
// Queremos crear un objeto igual a objA, pero con "prop2" igual a "newVal2" const objA = { prop1: 'val1', prop2: 'val2', }; const objB = objA; objB.prop2 = 'newVal2'; console.log(objB); // { prop1: 'val1', prop2: 'newVal2' } ✅ Tenemos nuestro nuevo objeto, pero... console.log(objA); // { prop1: 'val1', prop2: 'newVal2' } ❌ hemos modificado el objeto original console.log(objA === objB); // true ❌
Si queremos obtener un nuevo objecto, tan solo tenemos que hacer una destructuración del objeto, añadiendo las claves que queremos modificar, o bien usar
Object.assign
:
const objA = { prop1: 'val1', prop2: 'val2', }; const objB = { ...objA, prop2: 'newVal2'}; // o const objB = Object.assign({}, objA, { prop2: 'newVal2' }); console.log(objB); // { prop1: 'val1', prop2: 'newVal2' } ✅ console.log(objA); // { prop1: 'val1', prop2: 'val2' } ✅ console.log(objA === objB); // false ✅
Debemos llevar cuidado con métodos como
reverse
, splice
, etc, ya que mutan el array original. Una solución sería buscar un método alternativo o bien crear un nuevo array antes de llamar a esos métodos:
const options = ['a', 'b', 'c', 'd']; // en vez de splice(1, 2); const filteredOptions = options.filter(option => option !== 'b' && option !== 'c'); const reversedOptions = [...options].reverse();
Algunas librerías, como React, basan su filosofía en el concepto de immutabilidad, ya que los componentes son renderizados solo cuando alguna de las propiedades cambia.
En el caso anterior de efecto colateral, como
objA
es igual a objB
, si se pasase ese objeto como propiedad, podría suceder que el componente no fuese capaz de detectar esos cambios, y reflejase un estado desactualizado.
Con repecto a la transparencia referencial, imaginemos esta función:
const getsName = () => { try { return API.getUserName(); } catch (error) { return 'Unnamed'; } } console.log(getsName() || getDefaultName()); // API.getUserName() || getDefaultName();
Con esta definición, no solo no aprovechamos los beneficios de la programación funcional, sino que podríamos tener comportamientos no deseados. Por eso lo correcto sería:
const getName = (API) => { try { return API.getUserName(); } catch (error) { return 'Unnamed'; } } console.log(getName(API) || getDefaultName()); // getName(API) || getDefaultName();
Lo que estamos haciendo aquí es obligarnos a pasar todas las dependencias como argumentos a la función. En lugar de depender del valor que en cada momento tenga la variable
API
, estamos inyectando ese valor como argumento.
Así, garantizamos que podemos probar nuestra función en un entorno controlado, pasándole un mock a nuestra función, además de que para los mismos argumentos siempre tendremos las mismas salidas.
Supongamos que queremos crear una aplicación con la que podamos aplicar el mismo descuento a varios productos...
// aproximación por objeto const scooter = { brand: 'MyBrand', model: 'E-300', price: 1032.96, }; const applyDiscount = (percentage, product) => { return { ...product, price: product.price * (1 - percentage / 100), }; }; const summer20Discount = applyDiscount.bind(null, 20); const scooterWithDiscount = summer20Discount(scooter); console.log(scooter); // { brand: 'MyBrand', model: 'E-300', price: 1032.96 } console.log(scooterWithDiscount); // { brand: 'MyBrand', model: 'E-300', price: 826.368 }
Con
Function.prototype.bind
estamos fijando siempre el primero de los parámetros a 20, y así podemos hacer una aplicación parcial, es decir, ir pasando algunos argumentos a nuestra función, y posponer el resto para más adelante.
Así,
summer20Discount
sólamente necesita recibir el producto sobre el que se aplicará el descuento.
Curry va un paso más allá. Tomemos el ejemplo anterior. Al hacer:
// aproximación por objeto const applyDiscount = percentage => product => { return { ...product, price: product.price * (1 - percentage / 100), }; }; const summer20Discount = applyDiscount(20); const scooterWithDiscount = summer20Discount(scooter);
Vemos que la función
applyDiscount
retorna otra función, a la que le pasamos el producto como argumento. La ventaja de esta aproximación es que nos permite hacer cosas como esta:
const summer20DiscountAllProducts = products => products.map(applyDiscount(20)); const winter10DiscountAllProducts = products => products.map(applyDiscount(10)); const productsWithSummer20Discount = summer20DiscountAllProducts(products); const productsWithWinter10Discount = winter10DiscountAllProducts(products);
Estas aproximaciones son tan habituales en la programación funcional, que existen librerías especializadas en hacer este tipo de operaciones, como Ramda.
Una función de orden superior es aquella que o bien toma otra funciones como argumentos, o bien devuelven esa función como resultado (o ambas).
Es un concepto muy importante en programación funcional, porque gracias a ellas, podemos componer las funciones como si se tratase de operaciones matemáticas. Veamos un ejemplo.
const scooter = { brand: 'MyBrand', model: 'E-300', price: 1032.96, }; const addKeys = product => ({ ...product, createdAt: Date.now(), updatedAt: Date.now(), }); const formatPriceWithCurrency = product => ({ ...product, formattedPrice: `${product.price}€`, }); const addElegible = product => ({ ...product, isElegibleForShipping: product.price > 500, shippingPrice: product.price > 2000 ? 0 : 24.99, }); const pipe = (...fns) => value => fns.reduce((result, fn) => fn(result), value); const enhace = pipe( addKeys, formatPriceWithCurrency, addElegible, ); const enhacedScooter = enhace(scooter); console.log(enhacedScooter); /* { brand: 'MyBrand', model: 'E-300', price: 1032.96, createdAt: 1597066732592, updatedAt: 1597066732592, formattedPrice: '1032.96€', isElegibleForShipping: true, shippingPrice: 24.99 } */
Hemos creado una función,
pipe
, que toma como argumentos otras funciones, y nos devuelve otra función, a la que pasamos como argumento el valor inicial, y cuyo resultado es el resultado de aplicar todas las funciones sobre el valor inicial, en orden de izquierda a derecha.
Pero en realidad no necesitas escribir la implementación de
pipe
o compose
(lo mismo que pipe
pero en dirección opuesta), ya que existen multitud de librerías utilitarias, como Ramda, que vimos antes, que disponen de estas funciones, y otras mucho más potentes.
Hasta ahora, hemos visto ejemplos de funciones que devuelven valores concretos. Pero la mayoría de las aplicaciones de hoy en día tienen llamadas a APIs o bases de datos. Estas llamadas son asíncronas, y la práctica habitual es devolver un callback con el resultado, o utilizar Promise como interfaz para centralizar la resolución y evitar el callback hell.
La aproximación a la programación funcional es la misma, pero en lugar de trabajar con valores, vamos a trabajar con Promesas. Aunque Promise dispone del método
then
, que nos permite encadenar llamadas, a veces no es suficientemente expresivo.
Desde 2017, con ES2017/ES8, disponemos del estándar async/await, que nos permite trabajar con Promesas de forma similar a como trabajamos con valores.
const fetchUsers = (API) => { return API.fetch('/users').then(response => response.json()); }; // o const fetchUsers = async (API) => { const response = await API.fetch('/users'); return response.json(); };
Como vemos, ambas aproximaciones son fáciles de leer, pero la segunda aún más ya que su asincronía está "oculta". La complejidad aumenta mucho cuando introducimos operaciones sobre colecciones. Por ejemplo, imaginemos que queremos hacer llamadas en paralelo a distintos "endpoints" para cada uno de los usuarios:
// Se deben hacer todas las llamadas, obtener los resultados y lanzar otra llamada adicional const getStatsAndSendEmail = async (API, users) => { const usersStats = await Promise.all( users.map(async user => { const stats = await API.fetch(`/users/${user.id}/stats`).then(response => response.json(), ); return { id: user.id, stats, }; }), ); // Ahora puedo enviar un email con las estadísticas a cada usuario await API.fetch('/email', { method: 'POST', body: JSON.stringify({ usersStats }), }); return usersStats; };
Estas operaciones asíncronas se pueden encapsular en funciones y crear funciones más complejas a partir de ellas. Con ayuda de librerías utilitarias especializadas, como crocks o folktale, o bien utilizando mónadas personalizadas como Task, es posible llevar a cabo operaciones asíncronas manteniendo el código limpio y sencillo de entender.
En este artículo de introducción a la programación funcional en JavaScript, hemos conocido algunos conceptos básicos: qué son los argumentos y parámetros, qué es la aridad, qué son las funciones puras, qué son el curry y las funciones aplicadas parcialmente, y qué son las funciones de orden superior.
Estas herramientas te permitirán crear funciones más limpias y reusables, que podrás testear fácilmente, lo que te ayudará a crear aplicaciones más robustas, expresivas y fáciles de mantener.
Si alguno de estos conceptos no ha quedado claro, o te interesa que profundice en algún aspecto concreto de este tema, por favor házmelo saber dejando un comentario.
Si vas a empezar a utilizar programación funcional en tus proyectos, te recomiendo que empieces por intentar evitar los efectos colaterales, ya que es la fuente de errores más común. Y poco a poco, cuando te encuentres cómodo, vete introduciendo conceptos más complejos.
Al principio, puede resultar engorroso componer algunas funciones cuando estás acostumbrado al enfoque imperativo, pero con la práctica, conseguirás pensar como un programador funcional. Te recomendamos, mientras tanto, que utilices una librería, como Ramda o Lodash/fp (la versión de lodash con utilidades para programación funcional), con muchas utilidades y funciones preparadas para componer y transformar datos.