AboutOpinionesBlogLa Blockletter

Software Crafters® 2025 | Creado con 🖤 para elevar el nivel de la conversación sobre programación en español | Legal

Home » javascript » Programación funcional en JavaScript. Introducción
Programación funcional en JavaScript. Introducción

Programación funcional en JavaScript. Introducción

La programación funcional es un paradigma de programación basado en la composición de funciones matemáticas.

Jose Manuel Lucas · Seguir9 min read ·

Al suscribirte a la newsletter comparto contigo los 10 libros más importantes de programación. Los que sin duda todo dev debería leer al menos una vez...

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:

  • Es mucho más fácil de testear.
  • Se reduce la complejidad, al enfocarse en qué vamos a hacer, y no cómo lo haremos.
  • El código es más modular, y por lo tanto, más sencillo de entender.
  • También es más confiable, al tener la seguridad de que no cambiaremos el valor de ningún recurso compartido.

Algunos conceptos de la programación funcional

Parámetros y argumentos

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

Aridad

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;

Funciones puras

Una función es pura cuando:

  • Su salida depende sólamente de los parámetros recibidos (es determinista), por lo que una llamada a la función se podría sustituir por el valor que devuelve sin que el funcionamiento de la aplicación se viese alterado (transparencia referencial).
  • Los parámetros no son modificados y no se producen efectos colaterales. Imaginemos que varias partes de la aplicación apuntasen a la misma referencia que ha recibido nuestra función como argumento y que esta referencia original fuese alterada...

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.assign
:

const 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

reverse
,
splice
, etc, ya que mutan el array original. Una solución sería buscar un método alternativo o bien crear un nuevo array antes de llamar a esos métodos:

const 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

objA
es igual a
objB
, si se pasase ese objeto como propiedad, podría suceder que el componente no fuese capaz de detectar esos cambios, y reflejase un estado desactualizado.

Con 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

API
, estamos inyectando ese valor como argumento. Así, garantizamos que podemos probar nuestra función en un entorno controlado, pasándole un mock a nuestra función, además de que para los mismos argumentos siempre tendremos las mismas salidas.

Curry y aplicación parcial

Supongamos 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.bind
estamos fijando siempre el primero de los parámetros a 20, y así podemos hacer una aplicación parcial, es decir, ir pasando algunos argumentos a nuestra función, y posponer el resto para más adelante.

Así,

summer20Discount
sólamente necesita recibir el producto sobre el que se aplicará el descuento.

Curry 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

applyDiscount
retorna otra función, a la que le pasamos el producto como argumento. La ventaja de esta aproximación es que nos permite hacer cosas como esta:

const 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.

Función de orden superior

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,

pipe
, que toma como argumentos otras funciones, y nos devuelve otra función, a la que pasamos como argumento el valor inicial, y cuyo resultado es el resultado de aplicar todas las funciones sobre el valor inicial, en orden de izquierda a derecha.

Pero en realidad no necesitas escribir la implementación de

pipe
o
compose
(lo mismo que
pipe
pero en dirección opuesta), ya que existen multitud de librerías utilitarias, como Ramda, que vimos antes, que disponen de estas funciones, y otras mucho más potentes.

Programación funcional asíncrona

Hasta 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

then
, que nos permite encadenar llamadas, a veces no es suficientemente expresivo. Desde 2017, con ES2017/ES8, disponemos del estándar async/await, que nos permite trabajar con Promesas de forma similar a como trabajamos con valores.

const 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.

Conclusión

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.

Al suscribirte a la newsletter comparto contigo los 10 libros más importantes de programación. Los que sin duda todo dev debería leer al menos una vez...

Quizás te interese

Arquitectura Hexagonal en el FrontEnd

Arquitectura Hexagonal en el FrontEnd

Merkle Trees y prueba de inclusión con TypeScript y TDD

Merkle Trees y prueba de inclusión con TypeScript y TDD