Inicio / Artículos / Desmitificando el currying en JavaScript: cierres, aplicación parcial y reutilización

Desmitificando el currying en JavaScript: cierres, aplicación parcial y reutilización

Aprenda cómo el currying convierte funciones de JavaScript con múltiples argumentos en cadenas reutilizables con un solo argumento, cómo difiere de la aplicación parcial y cuándo omitirlo.

1051 palabras

Si sigues pasando siempre el mismo primer argumento a una función una y otra vez, se cobra un pequeño costo por repetición en cada llamada. El currying te permite fijar parte de los argumentos de una función una vez y reutilizar el resultado. A continuación verás qué es el currying, cómo las clausuras lo hacen posible, en qué se diferencia de la aplicación parcial y cuándo realmente mejora el código.

Qué significa el currying

El currying convierte una función que recibe varios argumentos en una secuencia de funciones, donde cada una recibe exactamente uno. Una llamada normal proporciona todo de una sola vez:

f(a, b, c)

La forma curried proporciona un valor por llamada, y cada llamada devuelve la siguiente función de la cadena:

f(a)(b)(c)

Cada función intermedia recuerda los valores de las llamadas anteriores. Esa memoria proviene de las clausuras, que se explican más abajo.

Convierte una función normal

Comienza con una operación de suma ordinaria que requiere dos argumentos:

function sum(a, b) {
  return a + b;
}

sum(2, 3); // 5

Un pequeño ayudante puede envolver cualquier función de dos argumentos: la función exterior toma el primer valor, devuelve una función que toma el segundo y solo entonces llama a la original con ambos:

function curry(fn) {
  return function (a) {
    return function (b) {
      return fn(a, b);
    };
  };
}

const curriedSum = curry(sum);
curriedSum(2)(3); // 5

Aspectos a tener en cuenta:

  • curry es de orden superior: tanto su entrada como su salida son funciones.
  • Cada función que devuelve espera un solo valor.
  • La función interior aún puede acceder a a después de que la llamada exterior haya finalizado, ya que mantiene su referencia.

Este ayudante solo maneja exactamente dos parámetros. Las versiones multiusos verifican fn.length y siguen recopilando argumentos hasta que haya suficientes, pero la idea es idéntica.

Un caso práctico: registradores especializados

La aritmética oculta los beneficios, así que considere un registrador que acepte un nivel de severidad y un mensaje:

function log(level, message) {
  console.log(`[${level}] ${message}`);
}

Los lugares donde se realizan las llamadas comienzan rápidamente a repetir el mismo nivel:

log("ERROR", "Something broke");
log("ERROR", "Invalid token");
log("ERROR", "API failed");

Un logger curried permite crear loggers preconfigurados una sola vez:

function log(level) {
  return function (message) {
    console.log(`[${level}] ${message}`);
  };
}

const errorLog = log("ERROR");
const infoLog = log("INFO");

errorLog("Something broke");
errorLog("Invalid token");
infoLog("User logged in");

El nivel se fija cuando se crean errorLog y infoLog. Los lugares de llamada se acortan, la intención se entiende fácilmente y las funciones especializadas pueden exportarse o injertarse en otro lugar. Esa combinación de reutilización y expresividad es la razón principal para usar el currying.

Aplicación parcial

La aplicación parcial significa rellenar previamente algunos argumentos de una función existente para obtener una nueva función, sin cambiar la estructura original. JavaScript lo soporta de forma nativa a través de bind:

function multiply(a, b) {
  return a * b;
}

const double = multiply.bind(null, 2);
double(5); // 10

El primer argumento de bind establece this, el cual multiply ignora, por lo que resulta en null. multiply sigue requiriendo dos argumentos; double es simplemente una variante en la que se proporciona el primero de ellos.

Currying versus aplicación parcial

Ambos comparten un objetivo pero no un mecanismo:

  • El currying reestructura una función en una cadena de funciones con un solo argumento.
  • La aplicación parcial completa algunos argumentos de inmediato y mantiene la estructura original de la función.

Cada paso de una cadena curriada es, en efecto, una aplicación parcial con un solo argumento, pero la aplicación parcial nunca requiere currying.

Currying abierto

Una variante lúdica sigue aceptando valores hasta que se finaliza la cadena con una llamada vacía:

let infiniteSum = function (a) {
  return function (b) {
    if (b !== undefined) return infiniteSum(a + b);
    return a;
  };
};

infiniteSum(1)(2)(3)(4)(); // 10

Así es como se despliega:

  • Cada llamada con un valor lo agrega al total acumulado y devuelve una nueva función que lleva ese total.
  • La última llamada a () deja b como undefined, por lo que se devuelve la suma.
  • Las cierres de función transportan el total de un paso al siguiente.

La verificación explícita b !== undefined es importante. Una prueba de verdad como if (b) trataría a 0 como el terminador, de modo que infiniteSum(1)(0)(2)() se detendría en cero en lugar de devolver 3. Este patrón es un excelente ejercicio sobre cierres de función y una pregunta común en entrevistas, pero la llamada final fácil de olvidar lo convierte en una mala opción por defecto para APIs de producción.

Las cierres de función hacen que todo funcione

Aquí está el sumador curried más simple:

function add(a) {
  return function (b) {
    return a + b;
  };
}

add(3)(4); // 7

add(3) devuelve la función interna y termina, pero esa función interna aún puede leer a. Eso es un cierre: una función mantiene el acceso a las variables del ámbito en el que fue definida, incluso después de que la función externa haya devuelto un valor. Dado que el valor capturado persiste, se puede almacenar y reutilizar la función parcialmente aplicada:

const add3 = add(3);

add3(4); // 7
add3(10); // 13

Para obtener más información sobre cómo se resuelven esas variables capturadas, consulte cómo resuelve la cadena de ámbitos las variables en JavaScript.

Cuándo usarlo y cuándo omitirlo

El currying es útil cuando:

  • los mismos argumentos iniciales aparecen en muchos puntos de llamada
  • hace que el código sea más claro en lugar de más enigmático
  • se desean funciones pequeñas, componibles y reutilizables
  • Se crean utilidades como registradores, validadores, formateadores o ayudantes de API, donde el valor de configuración se conoce con antelación
  • Omite este enfoque cuando:

    • una función simple y única no se beneficia al dividirla
    • tu equipo no está familiarizado con este patrón
    • las llamadas en cadena dificultan la legibilidad

    Conclusión

    El currying se trata realmente de ordenar las entradas de una función según el momento en que se conocen: la configuración, como el nivel de registro o la URL base, se fija temprano, mientras que los datos de cada llamada llegan más tarde. Las clausuras se encargan de recordar esto, bind permite una aplicación parcial más ligera cuando no se necesita una cadena completa, y la pregunta clave siempre es si el código de llamada resulta más fácil de leer y reutilizar.

    Lecturas relacionadas

  • Actualización a htmx 4: Fetch, herencia explícita y qué se rompe — Entienda los cambios en la arquitectura de htmx 4, desde el núcleo basado en Fetch y la herencia explícita de atributos hasta los intercambios de errores, el cambio de forma y el historial, y planifique una migración segura.