Programación funcional en JavaScript: de la teoría a la práctica en Node.js
La programación funcional (PF) es un paradigma que considera los cálculos como la evaluación de funciones matemáticas y evita cambiar el estado y los datos mutables. En el contexto de JavaScript y Node.js, la PF permite escribir código más predecible, testeable y fácil de mantener, especialmente al trabajar con flujos asíncronos y transformaciones complejas de datos. Este artículo está dirigido a desarrolladores que ya están familiarizados con los fundamentos de la PF y profundiza en conceptos avanzados: composición de funciones, functores, mónadas y gestión de efectos secundarios.
1. Funciones puras e inmutabilidad: el fundamento de la confiabilidad
A nivel avanzado, no basta con conocer la definición de una función pura. Es importante entender cómo la inmutabilidad y la ausencia de efectos secundarios afectan la arquitectura de la aplicación, especialmente en el entorno de Node.js, donde a menudo se utilizan operaciones concurrentes.
1.1. Copia profunda y uso compartido estructural (Structural Sharing)
La copia profunda completa de objetos grandes puede ser costosa. En su lugar, bibliotecas como Immer o Immutable.js utilizan el uso compartido estructural: al modificar una parte del objeto, se crea una nueva referencia, pero las partes no modificadas se comparten con el estado anterior.
// Ejemplo usando Immer (biblioteca popular)const { produce } = require('immer');
const baseState = { user: { name: 'Alice', age: 30 }, meta: { created: Date.now() }};
// produce crea un nuevo estado, sin mutar baseStateconst nextState = produce(baseState, draft => { draft.user.age = 31;});
console.log(baseState.user.age); // 30console.log(nextState.user.age); // 31console.log(baseState.meta === nextState.meta); // true (Structural Sharing)1.2. Funciones puras y memoización
Las funciones puras son ideales para la memoización: almacenar en caché los resultados para evitar cálculos repetidos. En Node.js, esto es crítico para cálculos pesados o consultas a la base de datos.
// Memoización simple para una función puraconst memoize = (fn) => { const cache = new Map(); return (...args) => { const key = JSON.stringify(args); if (cache.has(key)) { console.log('Desde caché'); return cache.get(key); } const result = fn(...args); cache.set(key, result); return result; };};
const fibonacci = (n) => (n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2));const memoizedFib = memoize(fibonacci);
console.log(memoizedFib(40)); // Se calculaconsole.log(memoizedFib(40)); // Desde caché2. Composición de funciones: construir lo complejo a partir de lo simple
La composición es el proceso de combinar dos o más funciones para obtener una nueva función. A nivel avanzado, no solo usamos composición simple, sino también patrones como Pipe y Compose, que son la base para construir pipelines de procesamiento de datos.
2.1. Pipe y Compose
Pipe ejecuta las funciones de izquierda a derecha, mientras que Compose las ejecuta de derecha a izquierda. En Node.js, esto se usa a menudo para procesar datos entrantes (por ejemplo, solicitudes HTTP).
// Implementación de pipeconst pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
// Ejemplo: procesamiento de una cadenaconst toLowerCase = (str) => str.toLowerCase();const removeSpaces = (str) => str.replace(/\\s+/g, '-');const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const slugify = pipe(toLowerCase, removeSpaces, capitalize);console.log(slugify('Hello World From Node.js')); // Hello-world-from-node.js2.2. Pipelines asíncronos
En aplicaciones reales de Node.js, las funciones suelen ser asíncronas. Creemos una versión de pipe para trabajar con promesas.
const asyncPipe = (...fns) => (x) => fns.reduce((acc, fn) => acc.then(fn), Pr