Kodokon kodokon.com

Patrones funcionales: currying, composición, inmutabilidad

Estructura tu código con currying, composición de funciones y datos inmutables para ganar previsibilidad y testabilidad.

9 min · 3 preguntas

Abrir esta lección en Kodokon

JavaScript trata las funciones como valores de primera clase, lo que abre la puerta a los patrones funcionales. El currying convierte una función de n argumentos en una cadena de funciones de un solo argumento: f(a, b, c) se convierte en f(a)(b)(c). No hay que confundirlo con la aplicación parcial, que fija algunos argumentos en un solo paso (fn.bind(null, a)). El currying brilla al crear funciones especializadas a partir de otras genéricas.

JAVASCRIPT
const curry = (fn) => {
  const arity = fn.length;
  return function curried(...args) {
    if (args.length >= arity) return fn(...args);
    return (...rest) => curried(...args, ...rest);
  };
};

const add3 = (a, b, c) => a + b + c;
const addCurried = curry(add3);

console.log(addCurried(1)(2)(3)); // 6
console.log(addCurried(1, 2)(3)); // 6
Un curry genérico basado en la aridad de la función.

Una sutileza de la especificación: fn.length solo cuenta los parámetros situados antes del primer parámetro por defecto o rest. (a, b = 1) => {} tiene una longitud de 1, (...args) => {} de 0. Por lo tanto, a un curry basado en la aridad hay que darle funciones con parámetros simples. La composición es el siguiente paso: ensamblar pequeñas funciones puras en tuberías legibles.

JAVASCRIPT
const pipe = (...fns) => (input) =>
  fns.reduce((acc, fn) => fn(acc), input);

const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const dashes = (s) => s.replace(/\s+/g, "-");

const toSlug = pipe(trim, lower, dashes);
console.log(toSlug("  Hello World  "));
// "hello-world"
pipe aplica las funciones de izquierda a derecha.

pipe(f, g, h)(x) calcula h(g(f(x))): lo lees en orden de ejecución. La variante compose aplica de derecha a izquierda, como la notación matemática. Estas tuberías solo tienen valor si las funciones son puras - sin efectos secundarios, sin dependencias ocultas - lo que exige datos inmutables: en lugar de mutar un objeto, produces una nueva versión de él, y la igualdad por referencia basta entonces para detectar un cambio (el corazón de la reconciliación de React).

JAVASCRIPT
const state = Object.freeze({
  user: { name: "Ada" },
  tags: ["js"]
});

const next = {
  ...state,
  tags: [...state.tags, "fp"]
};

const deepCopy = structuredClone(state);
deepCopy.user.name = "Grace"; // independent copy
Una nueva versión mediante copia en lugar de mutación.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Cuál es la diferencia entre currying y aplicación parcial?
    • Ninguna, son sinónimos
    • El currying convierte f(a, b, c) en f(a)(b)(c); la aplicación parcial fija algunos argumentos en una sola llamada
    • La aplicación parcial solo funciona con bind, el currying solo con funciones flecha
    • El currying solo se aplica a funciones de dos argumentos
  2. ¿A qué equivale pipe(f, g, h)(x)?
    • f(g(h(x)))
    • h(g(f(x)))
    • g(f(h(x)))
    • f(x) + g(x) + h(x)
  3. Después de const s = Object.freeze({ a: { b: 1 } }), ¿funciona la asignación s.a.b = 2?
    • No, freeze congela todo el árbol del objeto
    • No, se lanza un TypeError incluso fuera del modo estricto
    • Sí, porque freeze es superficial: el objeto anidado s.a no está congelado
    • Sí, pero solo en modo no estricto