TechLead
Lección 5 de 8

Composición de funciones

Combina funciones simples para construir operaciones complejas con compose y pipe

¿Qué es la composición de funciones?

La composición de funciones es el proceso de combinar dos o más funciones para producir una nueva función. Es como construir con LEGO: piezas simples se encajan para crear estructuras complejas. En matemáticas, la composición se escribe como (f ∘ g)(x) = f(g(x)).

// Manual composition
const add1 = x => x + 1;
const double = x => x * 2;
const square = x => x * x;

// Compose manually: square(double(add1(5)))
const result = square(double(add1(5)));
// add1(5) = 6, double(6) = 12, square(12) = 144

// Create a composed function
const transform = x => square(double(add1(x)));
transform(5); // 144

La función compose

compose combina funciones de derecha a izquierda (como en matemáticas).

// Simple compose for two functions
const compose2 = (f, g) => x => f(g(x));

const add1ThenDouble = compose2(double, add1);
add1ThenDouble(5); // double(add1(5)) = double(6) = 12

// Compose for any number of functions
const compose = (...fns) => x => 
  fns.reduceRight((acc, fn) => fn(acc), x);

// Read right-to-left: add1 → double → square
const transform = compose(square, double, add1);
transform(5); // 144

// More practical example
const sanitize = str => str.trim();
const lowercase = str => str.toLowerCase();
const slugify = str => str.replace(/\s+/g, '-');

const createSlug = compose(slugify, lowercase, sanitize);
createSlug('  Hello World  '); // 'hello-world'

La función pipe

pipe es como compose pero va de izquierda a derecha, lo cual suele ser más intuitivo.

// Pipe - left to right (data flows like water through pipes)
const pipe = (...fns) => x => 
  fns.reduce((acc, fn) => fn(acc), x);

// Read left-to-right: add1 → double → square
const transform = pipe(add1, double, square);
transform(5); // 144

// More readable for data transformations
const processUser = pipe(
  validateInput,
  normalizeEmail,
  hashPassword,
  saveToDatabase
);

// Practical example
const processText = pipe(
  str => str.trim(),
  str => str.toLowerCase(),
  str => str.split(' '),
  words => words.filter(w => w.length > 3),
  words => words.join(', ')
);

processText('  The QUICK Brown Fox  ');
// 'quick, brown'

Estilo point-free

El estilo point-free (o tácito) consiste en escribir funciones sin mencionar sus argumentos.

// With points (explicit argument)
const double = x => x * 2;
const numbers = [1, 2, 3];
const doubled = numbers.map(x => double(x));

// Point-free (no explicit argument)
const doubled = numbers.map(double);

// More examples
// With points
const getNames = users => users.map(user => user.name);

// Point-free with composition
const prop = key => obj => obj[key];
const getNames = users => users.map(prop('name'));

// Building point-free functions
const isEven = n => n % 2 === 0;
const not = fn => (...args) => !fn(...args);
const isOdd = not(isEven);

// Filter examples - point-free
const evens = numbers.filter(isEven);
const odds = numbers.filter(isOdd);

// Caution: Point-free isn't always clearer
// Sometimes explicit is better for readability

Patrones de composición en el mundo real

// Data validation pipeline
const validators = {
  required: value => value !== '' ? null : 'Required',
  email: value => /@/.test(value) ? null : 'Invalid email',
  minLength: min => value => 
    value.length >= min ? null : `Min ${min} chars`,
};

const validate = (...rules) => value =>
  rules.map(rule => rule(value)).filter(Boolean);

const validateEmail = validate(
  validators.required,
  validators.email,
  validators.minLength(5)
);

validateEmail('');         // ['Required', 'Invalid email', 'Min 5 chars']
validateEmail('ab');       // ['Invalid email', 'Min 5 chars']
validateEmail('a@b.com');  // []

// API response transformation
const processApiResponse = pipe(
  response => response.data,
  data => data.users,
  users => users.filter(u => u.active),
  users => users.map(u => ({ id: u.id, name: u.name })),
  users => users.sort((a, b) => a.name.localeCompare(b.name))
);

// Middleware pattern (like Express)
const applyMiddleware = (...middlewares) => handler =>
  middlewares.reduceRight(
    (next, middleware) => middleware(next),
    handler
  );

const withLogging = next => request => {
  console.log('Request:', request);
  return next(request);
};

const withAuth = next => request => {
  if (!request.token) throw new Error('Unauthorized');
  return next(request);
};

const handleRequest = applyMiddleware(
  withLogging,
  withAuth
)(request => ({ success: true }));

Composición asíncrona

// Compose async functions (Promises)
const composeAsync = (...fns) => x =>
  fns.reduceRight(
    (promise, fn) => promise.then(fn),
    Promise.resolve(x)
  );

const pipeAsync = (...fns) => x =>
  fns.reduce(
    (promise, fn) => promise.then(fn),
    Promise.resolve(x)
  );

// Example async pipeline
const fetchUser = async id => {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
};

const getPostsByUser = async user => {
  const res = await fetch(`/api/posts?userId=${user.id}`);
  return res.json();
};

const sortByDate = posts => 
  [...posts].sort((a, b) => new Date(b.date) - new Date(a.date));

const getUserPosts = pipeAsync(
  fetchUser,
  getPostsByUser,
  sortByDate
);

// Usage
const posts = await getUserPosts(123);

✅ Buenas prácticas de composición

  • • Mantén funciones pequeñas y enfocadas (una sola responsabilidad)
  • • Las funciones deberían aceptar un argumento cuando sea posible
  • • Usa pipe para lectura de izquierda a derecha
  • • Nombra las funciones compuestas de forma descriptiva
  • • Asegura que los tipos coincidan entre funciones compuestas
  • • Usa point-free solo cuando mejore la claridad