Introducción a la programación funcional en JavaScript — Parte 3: Composición

HOFs, Aplicación parcial y Composición de funciones

Lupo Montero
9 min readJul 9, 2018
Detalle de obra en proceso de Valeria Ghezzi

Este post es parte de la serie Introducción a programación funcional en JavaScript. Si todavía no has leído las partes 1 y 2 (Introducción, Funciones Puras), te recomiendo empezar por ahí 😉

En este post nos centraremos en la composición de funciones, pero antes de llegar ahí, exploraremos los conceptos de Higher-order Functions (HOFs) y aplicación parcial para entender mejor el concepto.

La composición de funciones es un mecanismo increíblemente poderoso a la hora de desarrollar software. En el mundo de JavaScript es una tendencia muy fuerte que vemos en herramientas como React o Redux.

ℹ Una nota sobre las funciones en JavaScript

En JavaScript podemos declarar funciones de varias maneras:

//
// Con el keyword `function`
//
// Declarando una función clásica "con nombre"
function double(a) {
return a * 2;
}
// Usando una expresión de función "con nombre"
const fn = function double(a) {
return a * 2;
};
// Usando una expresión de función "anónima"
const double = function (a) {
return a * 2;
};
//
// Con funciones flecha (arrow functions)
//
// Con retorno explícito
const double = (a) => {
return a * 2;
};
// Con retorno implícito y sin paréntesis opcionales en argumentos
// (ya que solo hay uno)
const double = a => a * 2;

En esta serie, usaremos funciones flecha (arrow functions) exclusivamente, y siempre que el cuerpo de nuestra función sea una expresión, usaremos retorno implícito. Asegúrate de familiarizarte con las funciones flecha antes de continuar…

Higher-order Functions (HOFs) / Funciones de orden superior

Una función de orden superior no es más que cualquier función que cumpla por lo menos una de las siguientes condiciones:

  • Recibe una función como argumento
  • Retorna una función

Como suele pasar con conceptos de programación funcional, al principio parece simple y no parece aportar mucho o tener grandes repercusiones. Pero como suele pasar, valga la redundancia, las repercusiones y el poder que se derivan de esta cualidad son enormes. En el caso de la composición, es gracias a que tenemos HOFs que podemos tomar funciones como inputs y producir una nueva función.

Pasando funciones como argumentos

Si ya estás familiarizada con JavaScript, probablemente hayas usado HOFs al pasar funciones como argumentos a otras funciones, como al hacer operaciones asíncronas (pasando un callback, registrando un listener, o pasando una función al .then() o .catch() de una promesa), o al usar los métodos .map(), .filter() o reduce() para transformar arreglos.

// Pasando callback como "event listener"
someDomNode.addEventListener('click', (event) => {
// ...
});
// Pasando callbacks a promise.then() y promise.catch()
fetch('http://api.laboratoria.la/')
.then(response => response.json())
.then(console.log)
.catch(console.error);
// Pasando funciones a Array.prototype.map
const names = [
{ name: 'one' },
{ name: 'two' },
{ name: 'three' },
].map(obj => obj.name);
// => ["one", "two", "three"]

Retornando funciones

En JavaScript, las funciones son un tipo de valor como cualquier otro, los podemos asignar (a variables y propiedades de objetos), y también las podemos retornar como resultado de una función (valor de retorno).

// Una función que retorna una función (usando bloques y retorno 
// explícito para ilustrar el concepto).
const fn = () => {
return () => {
return true;
};
};
// La misma función usando retornos implícitos
const fn = () => () => true;

Ahora podemos invocar nuestra función (en un test usando Jest por ejemplo):

describe('fn', () => {
it('should be a function that returns a function', () => {
expect(typeof fn).toBe('function');
expect(typeof fn()).toBe('function');
});
it('should return a function that returns true', () => {
expect(fn()()).toBe(true);
});
});

Aplicación parcial

Ahora que ya hemos visto que podemos tanto recibir funciones como argumentos como retornar funciones, veamos un concepto común en la programación funcional: aplicación parcial (partial application en inglés).

Este concepto hace referencia al proceso de fijar uno o más argumentos a una función, produciendo una nueva función de menor aridad. Esto significa que si tenemos una función que espera varios argumentos, podemos usar aplicación parcial para fijar uno (o varios), y producir una nueva función que tiene el mismo comportamiento que la primera, pero con alguno de los argumentos ya fijados. Veamos un ejemplo:

// Sin aplicación parcial
const greet = (greeting, name) => `${greeting}, ${name}`;
// Usando aplicación parcial para fijar el primer argumento
const greetPart = greeting => name => `${greeting}, ${name}`;
const greetHello = greetPart('Hello');greetHello('Heidi') // => 'Hello, Heidi'

La función greetPart espera un argumento (greeting) y retorna otra función, que a su vez espera otro argumento (name) y retorna un string. En este caso, podemos ver que la función greetPart se parece mucho a la función greet, de hecho nos permiten hacer lo mismo, pero greetPart nos permite separar la invocación a greet en dos invocaciones, cada una haciéndose cargo de un argumento, lo que nos permitiría preparar la función con parte de los argumentos ya fijados para más adelante poder usarla sin tener que especificar todos sus argumentos.

Veamos ahora cómo podríamos implementar una función para generalizar el concepto de aplicación parcial en JavaScript. Con la siguiente función partial() podemos aplicar parcialmente los argumentos que queramos de una función:

// Recibe la función a la que le queremos "aplicar" argumentos
// y los argumentos que queremos "aplicar".
const partial = (fun, ...args) =>
(...newArgs) => fun(...args.concat(newArgs));
// Una función genérica que recibe varios argumentos
const greeter = (greeting, separator, emphasis, name) =>
`${greeting}${separator}${name}${emphasis}`;
// Creamos una nueva función con los primeros 3 argumentos aplicados
const greetHello = partial(greeter, 'Hello', ', ', '.');
// Invocamos la función parcialmente aplicada pasando el último
// argumento.
greetHello('Heidi');
// => 'Hello, Heidi.'

Composición

En ciencias de la computación, hablamos de composición de funciones cuando combinamos funciones para producir nuevas funciones de mayor complejidad. La composición de funciones no debe confundirse con la composición de objetos, que es una manera de combinar objetos (estructuras).

Empecemos por una analogía, imagina que toda función es como un pedacito de tubería, todas tienen dos aberturas en los extremos, una entrada y una salida…

Una función como una sección de tubería.

Ahora, si tenemos varias tuberías y todas son del mismo grosor (tienen la misma interfaz), las podemos conectar.

Data fluyendo a través de funciones.

De esta forma estamos conectando la salida de la primera función a la entrada de la siguiente, y la salida de ésta a la entrada de la que sigue y así sucesivamente. De hecho, así resulta más fácil visualizar como al ir encadenando las tuberías (funciones) va a ir fluyendo el agua (la data).

Cambiemos agua por un string. Implementemos 3 funciones que reciben un string y retornan un string (una interfaz consistente).

const toUpper = str => str.toUpperCase();const replaceSpacesWithDashes = str => str.replace(/\s/g, '-');const addBox = str => [
'-'.repeat(str.length + 4),
`| ${str} |`,
'-'.repeat(str.length + 4),
].join('\n');

Ahora, dado que las tres funciones tienen la misma interfaz (firma — signature), podemos fácilmente canalizar el output de una a otra y así combinar su funcionalidad.

console.log(
addBox(
replaceSpacesWithDashes(
toUpper('hello world')
)
)
);
// prints the following in the console:
// ---------------
// | HELLO-WORLD |
// ---------------

Si usamos esta combinación con frecuencia podríamos encapsularla en una nueva función que invoca a las 3 funciones en cuestión.

const processString = str => addBox(
replaceSpacesWithDashes(
toUpper('hello world')
)
);

Si nos fijamos en la implementación de procesString(), estamos anidando transformaciones (invocaciones), lo cual crea una estructura rígida, con un orden determinado. Si necesitáramos más flexibilidad para crear diferentes combinaciones, podríamos usar composición por medio de una utilidad para combinar las funciones… hello compose() !! 🚀

Imaginemos que pudiéramos hacer algo así:

// Usando una utilidad para componer de forma flexible
const composed = compose(addBox, replaceSpacesWithDashes, toUpper);
console.log(composed('hello world'))

Con esta nueva utilidad podemos crear todas las combinaciones que queramos, en el orden que queramos. Mucho mejor, no? 😎

Ahora veamos como podemos implementar una función compose() que nos permita hacer esto:

const compose =
(...fns) =>
(...args) =>
fns.slice(0, -1).reverse().reduce(
(memo, fn) => fn(memo),
fns[fns.length - 1](...args),
);

Voilá! Esta implementación de compose hace lo mismo que la que viene con Redux (creo?): recibe un número indeterminado de funciones como argumentos y retorna una función. La función retornada espera un número indeterminado de argumentos, que serán aplicados a la última función recibida en el primer paso (el último elemento de fns), y de ahí en adelante vamos a ir pasando la salida (output) de cada función en fns a la entrada de la siguiente… tal como hicimos con las tuberías.

Ya que estamos en el tema, veamos otro ejemplo inspirado en Redux: la función combineReducers(), la cual también podemos implementar con un .reduce para ir recorriendo una colección de funciones (reducers en este caso) y conectando la salida de una a la entrada de la siguiente, y así state va a ir fluyendo de un reductor al siguiente:

const combineReducers =
reducers =>
(state, action) =>
Object.keys(reducers).reduce((memo, key) => ({
...memo,
[key]: reducers[key](memo[key], action),
}), state);

En este artículo hemos hablado de HOFs (Higher-order Functions), aplicación parcial y composición, que son tres de los pilares en arquitecturas modernas como React y Redux. En este post no podía faltar por lo menos un ejemplo con React:

const withFooProp = WrappedComponent =>
props => <WrappedComponent foo={2} {...props} />;
const Bar = props => <div>{console.log(props)}</div>;const Baz = withFooProp(Bar); // augmented version of Barconst App = props => (
<div>
<Baz baz={2} />
</div>
);

En el ejemplo de arriba, tenemos un componente App que hace uso de otro componente que se llama Baz. El componente App está declarado como una función literal, pero si nos fijamos en Baz, vemos que se ha creado invocando a la función withFooProp(). Esta función withFooProp recibe un argumento, una función/componente, y retorna una función/componente. La función withFooProp es una HOF, y en el mundo de React diríamos que es un Higher-order Component (HOC), ya que recibe un componente como argumento y retorna un componente. Gracias a que todos los componentes tienen la misma interfaz (una función que retorna virtual DOM), podemos usar composición para combinar componentes.

const Hello = props => <div>{props.greeting} {props.name}!</div>const withGreeting =
greeting =>
Component =>
props => <Component {...props} greeting={greeting} />;
const withName =
name =>
Component =>
props => <Component {...props} name={name} />;
const HelloWithGreetingAndName = compose(
withGreeting('hello'),
withName('Lupo'),
)(Hello);

Este tipo de patrón es común para ir inyectando propiedades a componentes a través de composición.

const withData = options => {
// Sacar o construir data de alguna forma...
const data = {};
// retorna un HOC que retorna un componente que va a invocar al
// primero inyectándole la propiedad `data`
return Component => props => <Component {...props} data={data} />;
};

Currying

Para concluir, no podemos dejar de mencionar currying, que consiste en reemplazar una función que toma varios argumentos con una secuencia de funciones, cada una con un solo argumento. Podríamos decir que el currying es un caso de aplicación parcial en el que siempre rompemos todos los argumentos en una cadena de invocaciones, uno por uno, mientras que en la aplicación parcial normalmente solo fijamos el primer argumento, o un número fijo. Se trata de mecanismos similares pero sutilmente distintos.

Veamos un ejemplo de currying:

const sum3 = (x, y, z) => x + y + z;
console.log(sum3(1, 2, 3); // 6
const sum3Curried = x => y => z => x + y + z;
console.log(sum3Curried(1)(2)(3)); // 6

Esto lo podríamos generalizar en una función curry(), que dada una función fn, retorne una nueva función que espera un número indeterminado de argumentos, compara el número de argumentos con la aridad de la función fn (número de argumentos que espera la función), y si ya tenemos todos los argumentos necesarios invocamos la función fn, si no que aplicamos parcialmente los argumentos recibidos y volvemos a invocar curry recursivamente pasando la función parcialmente aplicada.

const curry = fn => (...xs) => (
(xs.length >= fn.length)
? fn(...xs)
: curry(fn.bind(null, ...xs))
);

describe('curry', () => {
it('should ...', () => {
const sum3 = (x, y, z) => x + y + z;
const curried = curry(sum3);
});
});

const curry = fn => (...xs) => (
(xs.length >= fn.length)
? fn(...xs)
: curry(partial(fn, ...xs))
);

Este post es parte de la serie Introducción a programación funcional en JavaScript. Si quieres continuar leyendo…

<< Parte 2: Funciones puras | Parte 4: Inmutabilidad (pronto 😉) >>

--

--