Capítulo 05: Programación por Composición
Cría Selectiva de Funciones
Aquí está compose:
const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];... ¡No te asustes! Esta es la forma nivel-9000-super-Saiyan de compose. Para facilitar el razonamiento, abandonemos la implementación variádica y consideremos una forma simplificada que componga dos funciones. Una vez que comprendas esto, podrás extender la abstracción y asumir que funciona para cualquier número de funciones (¡incluso podríamos demostrarlo!). Aquí tienes una versión más amigable de compose para ti, querido lector:
const compose2 = (f, g) => x => f(g(x));f y g son funciones, y x es el valor que fluye a través de ellas.
La composición se siente como cría selectiva de funciones. Tú, criador de funciones, seleccionas dos con características deseables, las fusionas y generas una nueva. Su uso es así:
const toUpperCase = x => x.toUpperCase();
const exclaim = x => `${x}!`;
const shout = compose(exclaim, toUpperCase);
shout('send in the clowns'); // "SEND IN THE CLOWNS!"La composición de dos funciones devuelve una nueva función. Esto es lógico: componer dos unidades de un tipo (en este caso funciones) debe producir otra unidad del mismo tipo. No conectas dos Legos y obtienes un Troncos de Lincoln [N.T. Juego de construcción de madera similar a LEGO]. Aquí hay una teoría subyacente que descubriremos a su tiempo.
En nuestra definición de compose, g se ejecuta antes que f, creando un flujo de datos de derecha a izquierda. Esto es más legible que anidar múltiples llamadas a funciones. Sin compose, el código sería:
const shout = x => exclaim(toUpperCase(x));En lugar de ejecutar de adentro hacia afuera, lo hacemos de derecha a izquierda, un paso hacia el lado oscuro (¡buh!). Veamos un ejemplo donde el orden importa:
const head = x => x[0];
const reverse = reduce((acc, x) => [x, ...acc], []);
const last = compose(head, reverse);
last(['jumpkick', 'roundhouse', 'uppercut']); // 'uppercut'reverse invierte la lista mientras head toma el primer elemento. Esto resulta en una función last efectiva pero ineficiente. El orden en la composición es clave. Podríamos definir una versión izquierda-derecha, pero respetamos la notación matemática original. Así es, la composición viene directo de los libros de matemáticas. Es hora de examinar una propiedad fundamental.
// associativity
compose(f, compose(g, h)) === compose(compose(f, g), h);La composición es asociativa, es decir, el agrupamiento no afecta el resultado. Si queremos poner un texto en mayúsculas, podemos escribir:
compose(toUpperCase, compose(head, reverse));
// or
compose(compose(toUpperCase, head), reverse);Dado que el agrupamiento es irrelevante, podemos usar una versión variádica de compose así:
// previously we'd have to write two composes, but since it's associative,
// we can give compose as many fn's as we like and let it decide how to group them.
const arg = ['jumpkick', 'roundhouse', 'uppercut'];
const lastUpper = compose(toUpperCase, head, reverse);
const loudLastUpper = compose(exclaim, toUpperCase, head, reverse);
lastUpper(arg); // 'UPPERCUT'
loudLastUpper(arg); // 'UPPERCUT!'La propiedad asociativa nos brinda flexibilidad y certeza de equivalencia. La implementación variádica más compleja está incluida en las librerías de soporte y es común en lodash, underscore y ramda.
Un beneficio de la asociatividad es poder agrupar funciones en composiciones específicas. Ejemplo de refactorización:
const loudLastUpper = compose(exclaim, toUpperCase, head, reverse);
// -- or ---------------------------------------------------------------
const last = compose(head, reverse);
const loudLastUpper = compose(exclaim, toUpperCase, last);
// -- or ---------------------------------------------------------------
const last = compose(head, reverse);
const angry = compose(exclaim, toUpperCase);
const loudLastUpper = compose(angry, last);
// more variations...No hay respuestas correctas o incorrectas. Simplemente conectamos Legos a nuestro gusto. Es útil agrupar funciones reutilizables como last o angry. Siguiendo el estilo de "Refactorización", reconocerás el proceso de "extraer función" sin estados complicados.
Estilo Pointfree
El estilo pointfree evita mencionar explícitamente los datos. Usa funciones de primera clase, currificación y composición para lograrlo.
Pista: Las versiones pointfree de
replaceytoLowerCaseestán en el Apéndice C - Utilidades Pointfree. ¡Revísalas!
// not pointfree because we mention the data: word
const snakeCase = word => word.toLowerCase().replace(/\s+/ig, '_');
// pointfree
const snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);¿Viste cómo aplicamos parcialmente replace? Los datos fluyen por funciones de un argumento. La currificación prepara cada función para procesar y pasar los datos. En el estilo pointfree, no necesitamos datos para construir la función, a diferencia del enfoque convencional.
Veamos otro ejemplo.
// not pointfree because we mention the data: name
const initials = name => name.split(' ').map(compose(toUpperCase, head)).join('. ');
// pointfree
// NOTE: we use 'intercalate' from the appendix instead of 'join' introduced in Chapter 09!
const initials = compose(intercalate('. '), map(compose(toUpperCase, head)), split(' '));
initials('hunter stockton thompson'); // 'H. S. T'El código pointfree elimina nombres redundantes y mantiene la concisión. Es un buen indicador de código funcional puro. Sin embargo, puede oscurecer la intención si se abusa. No todo el código funcional debe ser pointfree.
Depuración
Un error común es componer map (que requiere dos argumentos) sin aplicar currificación primero.
// wrong - we end up giving angry an array and we partially applied map with who knows what.
const latin = compose(map, angry, reverse);
latin(['frog', 'eyes']); // error
// right - each function expects 1 argument.
const latin = compose(map(angry), reverse);
latin(['frog', 'eyes']); // ['EYES!', 'FROG!'])Para depurar composiciones, usa esta función impura pero útil trace:
const trace = curry((tag, x) => {
console.log(tag, x);
return x;
});
const dasherize = compose(
intercalate('-'),
toLower,
split(' '),
replace(/\s{2,}/ig, ' '),
);
dasherize('The world is a vampire');
// TypeError: Cannot read property 'apply' of undefinedAlgo anda mal, usemos trace:
const dasherize = compose(
intercalate('-'),
toLower,
trace('after split'),
split(' '),
replace(/\s{2,}/ig, ' '),
);
dasherize('The world is a vampire');
// after split [ 'The', 'world', 'is', 'a', 'vampire' ]¡Ah! Debemos aplicar map a toLower porque opera en un arreglo.
const dasherize = compose(
intercalate('-'),
map(toLower),
split(' '),
replace(/\s{2,}/ig, ' '),
);
dasherize('The world is a vampire'); // 'the-world-is-a-vampire'trace permite inspeccionar datos durante la depuración. Lenguajes como Haskell y PureScript incluyen herramientas similares.
La composición es nuestra herramienta principal, respaldada por una teoría robusta que garantiza su eficacia. Profundicemos.
Teoría de Categorías
La teoría de categorías formaliza conceptos de teoría de conjuntos, tipos, grupos y lógica. Trata objetos, morfismos y transformaciones, que reflejan conceptos de programación. Esta tabla compara teorías:

No temas. No esperas dominar todos estos conceptos, pero la duplicación muestra por qué la teoría de categorías busca unificarlos.
Una categoría se define como:
- Colección de objetos
- Colección de morfismos
- Noción de composición entre morfismos
- Un morfismo distinguido: identidad
Aplicando esto a tipos y funciones (lo que nos interesa):
Colección de objetos Los objetos son tipos de datos: String, Boolean, Number, etc. Los tipos pueden verse como conjuntos de valores posibles (teoría de conjuntos). Ejemplo: Boolean es [true, false]. La teoría de conjuntos aplica aquí.
Colección de morfismos Los morfismos son funciones puras convencionales.
Composición de morfismos Nuestro compose, que es asociativo según exige la teoría de categorías.
Diagrama de composición:


Ejemplo en código:
const g = x => x.length;
const f = x => x === 4;
const isFourLetterWord = compose(f, g);Morfismo identidad Presentamos id, una función que devuelve su entrada:
const id = x => x;¿Para qué sirve? La usaremos extensivamente en próximos capítulos. Por ahora, imagínala como un sustituto de valores en código pointfree.
id debe integrarse con compose. Para toda función unaria f, se cumple:
// identity
compose(id, f) === compose(f, id) === f;
// true¡Como la identidad numérica! Si no es claro, reflexiona. Pronto verás id en acción, especialmente en código pointfree.
Ahora tienes una categoría de tipos y funciones. Si es tu primer contacto, la utilidad puede no ser evidente. A lo largo del libro, exploraremos cómo esta teoría brinda sabiduría sobre composición (asociatividad e identidad).
¿Otras categorías? Grafos dirigidos (objetos=nodos, morfismos=aristas), órdenes parciales (objetos=números, morfismos=>=), etc. Nos enfocaremos en la categoría mencionada.
Resumen
La composición conecta funciones como tuberías. Los datos fluyen necesariamente: las funciones puras transforman entradas en salidas. Romper este flujo inutiliza el software.
Priorizamos la composición como principio de diseño: mantiene aplicaciones simples y coherentes. La teoría de categorías influirá en arquitectura, efectos secundarios y corrección.
Es hora de ver esto en práctica. Construyamos una aplicación de ejemplo.
Capítulo 06: Aplicación de Ejemplo
Ejercicios
En los ejercicios, usaremos objetos Car con esta estructura:
{
name: 'Aston Martin One-77',
horsepower: 750,
dollar_value: 1850000,
in_stock: true,
}¡Practiquemos!
Reescribe la siguiente función usando compose().
const isLastInStock = (cars) => {
const lastCar = last(cars);
return prop('in_stock', lastCar);
};
Dada la función:
const average = xs => reduce(add, 0, xs) / xs.length;¡Practiquemos!
Refactoriza averageDollarValue usando average en estilo de composición.
const averageDollarValue = (cars) => {
const dollarValues = map(c => c.dollar_value, cars);
return average(dollarValues);
};
¡Practiquemos!
Refactoriza fastestCar con compose() y funciones pointfree. Pista: usa append.
const fastestCar = (cars) => {
const sorted = sortBy(car => car.horsepower, cars);
const fastest = last(sorted);
return concat(fastest.name, ' is the fastest');
};