La programación funcional en JavaScript no requiere un cambio radical de paradigma ni adoptar una biblioteca especializada. Con ES6+ se pueden aplicar sus principios fundamentales funciones puras, inmutabilidad, composición y currying de forma gradual, mejorando la testabilidad y la legibilidad del código.
Funciones puras: sin efectos secundarios
Una función pura siempre devuelve el mismo resultado para los mismos argumentos y no modifica nada fuera de su ámbito. Son predecibles, fáciles de testear y se pueden memorizar:
// Impura depende de estado externo y modifica un array
const historial = [];
function impura_registrar(accion) {
historial.push(accion); // Efecto secundario
return new Date().getTime(); // Resultado no determinista
}
// Pura mismo input ? mismo output, sin efectos
function calcularDescuento(precio, porcentaje) {
if (precio < 0 || porcentaje < 0 || porcentaje > 100) {
throw new RangeError('Parámetros inválidos');
}
return precio * (1 - porcentaje / 100);
}
// Fácil de testear:
console.log(calcularDescuento(100, 20)); // 80 siempre
console.log(calcularDescuento(100, 20)); // 80 siempre
Inmutabilidad con spread operator
Trabajar con datos inmutables evita bugs difíciles de rastrear causados por mutaciones inesperadas. El spread operator y los métodos inmutables de array facilitan crear nuevas versiones sin tocar el original:
// Actualizar un elemento en un array sin mutarlo
function actualizarItem(lista, id, cambios) {
return lista.map(item =>
item.id === id ? { ...item, ...cambios } : item
);
}
// Añadir un elemento
function añadir(lista, item) {
return [...lista, item];
}
// Eliminar un elemento
function eliminar(lista, id) {
return lista.filter(item => item.id !== id);
}
const tareas = [
{ id: 1, texto: 'Diseño', completada: false },
{ id: 2, texto: 'Desarrollo', completada: false },
];
const actualizadas = actualizarItem(tareas, 1, { completada: true });
console.log(tareas[0].completada); // false sin cambios
console.log(actualizadas[0].completada); // true
Currying genérico
El currying transforma una función de múltiples argumentos en una cadena de funciones que aceptan un argumento cada una. Permite crear versiones especializadas de funciones reutilizables:
// Curry manual para 2 y 3 argumentos
const curry2 = fn => a => b => fn(a, b);
const curry3 = fn => a => b => c => fn(a, b, c);
// Curry genérico para cualquier número de argumentos
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return function(...masArgs) {
return curried(...args, ...masArgs);
};
};
}
// Ejemplo de uso
const sumar = curry((a, b) => a + b);
const sumar5 = sumar(5);
console.log(sumar5(3)); // 8
console.log(sumar5(10)); // 15
const filtrarPorProp = curry((prop, valor, array) =>
array.filter(item => item[prop] === valor)
);
const soloActivos = filtrarPorProp('activo', true);
const usuarios = [
{ nombre: 'Ana', activo: true },
{ nombre: 'Bob', activo: false },
{ nombre: 'Carlos', activo: true },
];
console.log(soloActivos(usuarios).map(u => u.nombre)); // ['Ana', 'Carlos']
Composición con pipe y compose
pipe encadena funciones de izquierda a derecha (como las tuberías Unix); compose lo hace de derecha a izquierda (orden matemático). Ambas aplican cada función al resultado de la anterior:
const pipe = (...fns) => valor => fns.reduce((acc, fn) => fn(acc), valor);
const compose = (...fns) => valor => fns.reduceRight((acc, fn) => fn(acc), valor);
// Transformaciones reutilizables
const limpiar = str => str.trim();
const minusculas = str => str.toLowerCase();
const sinEspacios = str => str.replace(/s+/g, '-');
const sinAcentos = str => str.normalize('NFD').replace(/[?-?]/g, '');
const crearSlug = pipe(limpiar, minusculas, sinAcentos, sinEspacios);
console.log(crearSlug(' Programación Funcional en JavaScript '));
// 'programacion-funcional-en-javascript'
// Transformar un array de usuarios
const procesarUsuarios = pipe(
usuarios => usuarios.filter(u => u.activo),
usuarios => usuarios.map(u => ({ ...u, nombre: u.nombre.toUpperCase() })),
usuarios => usuarios.sort((a, b) => a.nombre.localeCompare(b.nombre))
);
const resultado = procesarUsuarios([
{ nombre: 'carlos', activo: true },
{ nombre: 'ana', activo: false },
{ nombre: 'beatriz', activo: true },
]);
// [{ nombre: 'BEATRIZ', activo: true }, { nombre: 'CARLOS', activo: true }]
Memoización
La memoización cachea el resultado de una función pura para evitar recalcular operaciones costosas con los mismos argumentos:
function memoize(fn) {
const cache = new Map();
return function(...args) {
const clave = JSON.stringify(args);
if (cache.has(clave)) {
return cache.get(clave);
}
const resultado = fn(...args);
cache.set(clave, resultado);
return resultado;
};
}
const fibonacci = memoize(function fib(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.time('fib');
console.log(fibonacci(40)); // 102334155
console.timeEnd('fib'); // < 1ms gracias al cache
El patrón Maybe: eliminar null checks
El patrón Maybe (o Option) envuelve un valor que puede ser null/undefined y permite encadenar operaciones sin comprobar null en cada paso:
class Maybe {
constructor(valor) {
this._valor = valor;
}
static of(valor) {
return new Maybe(valor);
}
static vacio() {
return new Maybe(null);
}
esVacio() {
return this._valor === null || this._valor === undefined;
}
map(fn) {
return this.esVacio() ? Maybe.vacio() : Maybe.of(fn(this._valor));
}
getOrElse(porDefecto) {
return this.esVacio() ? porDefecto : this._valor;
}
}
// Sin Maybe lleno de null checks
function obtenerCiudad(usuario) {
if (!usuario) return 'Desconocida';
if (!usuario.direccion) return 'Desconocida';
if (!usuario.direccion.ciudad) return 'Desconocida';
return usuario.direccion.ciudad;
}
// Con Maybe encadenado y legible
const obtenerCiudadM = usuario =>
Maybe.of(usuario)
.map(u => u.direccion)
.map(d => d.ciudad)
.getOrElse('Desconocida');
console.log(obtenerCiudadM(null)); // 'Desconocida'
console.log(obtenerCiudadM({ nombre: 'Ana' })); // 'Desconocida'
console.log(obtenerCiudadM({ direccion: { ciudad: 'Madrid' } })); // 'Madrid'
Antipatrones habituales al escribir código funcional
Algunos errores frecuentes al intentar escribir código funcional en JavaScript:
// MALO: map con efectos secundarios
const efectos = [];
[1, 2, 3].map(n => { efectos.push(n * 2); return n; }); // Usar forEach
// MALO: reduce que acumula mutando
const resultadoMal = [1, 2, 3].reduce((acc, n) => {
acc.push(n * 2); // Muta el acumulador
return acc;
}, []);
// BIEN: spread para inmutabilidad
const resultadoBien = [1, 2, 3].reduce((acc, n) => [...acc, n * 2], []);
// MALO: pasar métodos de objeto como callbacks sin bind
const obj = { multiplicador: 3, calcular(n) { return n * this.multiplicador; } };
[1, 2, 3].map(obj.calcular); // this es undefined NaN
// BIEN: usar arrow function o bind
[1, 2, 3].map(n => obj.calcular(n));
[1, 2, 3].map(obj.calcular.bind(obj));
La programación funcional en JavaScript no es un todo o nada. Aplicar funciones puras donde tiene sentido, evitar mutaciones en transformaciones de datos y usar composición para combinar transformaciones simples en pipelines legibles mejora significativamente la calidad del código sin necesidad de adoptar ninguna biblioteca ni cambiar toda la arquitectura.
