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.assignconst 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
reversespliceconst 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
objAobjBCon 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
APISupongamos 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.bindAsí,
summer20DiscountCurry 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
applyDiscountconst 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,
pipePero en realidad no necesitas escribir la implementación de
pipecomposepipeHasta 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
thenconst 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.