En un artÃculo anterior les hablÃĐ del mÃĐtodo .reduce, cÃģmo funciona y el caso ideal en el que podemos usarlo (en mi opinion). Esta vez voy a mostrar mÃĄs casos en los que podrÃa ser una buena opciÃģn. Ahora bien, no tienen que haber leÃdo ese artÃculo pero de aquà en adelante voy a asumir que saben cÃģmo funciona el mÃĐtodo Array.reduce. Al finalizar espero que aprendan a reconocer en quÃĐ lugares .reduce podrÃa funcionar perfectamente.
ÂŋQuÃĐ estamos buscando?
Patrones, buscamos patrones. Bueno... sÃģlo uno. Y para saber quÃĐ estamos buscando tenemos que ver los requerimientos de un reducer. Piensen en ellos por un momento, cuando empiezan a escribir uno que quieren usar con Array.reduce tal vez luce asÃ.
function (accumulator, value) {
/*
algo de lÃģgica por aquÃ
*/
return accumulator;
}
Okey, por lo general retornamos una copia modificada de accumulator pero eso no es importante, el punto es que retornamos el mismo "tipo" de dato que obtuvimos en el primer parÃĄmetro. Entonces tenemos que la comportamiento de la funciÃģn es el siguiente.
(Accumulator, Value) -> Accumulator
Pero en este caso lo que tenemos aquà es un ejemplo concreto. Yo quiero que vean esto de una forma mÃĄs abstracta. Lo que en realidad buscamos son funciones con esta forma.
(A, B) -> A
Eso es bÃĄsicamente todo lo que necesitan saber. Para que un reduce pueda hacer bien su trabajo sÃģlo debe ser capaz de retornar el mismo tipo de dato que recibiÃģ en el primer parÃĄmetro.
ÂŋAÚn estÃĄn confundidos? No se preocupen, pasaremos lo que queda de este artÃculo revisando ejemplos donde este patrÃģn puede aparecer.
Casos de uso
Acumuladores
Aquà generalmente es la parte donde les muestro una situaciÃģn donde sumamos un arreglo de nÚmeros o algo parecido. No hagamos eso. Podemos imaginarnos un escenario mÃĄs complejo donde un acumulador nos sea Útil.
Entonces, vamos a fingir que estamos trabajando en un proyecto que tiene una especie de blog y nos encontramos creando la pÃĄgina del perfil de usuario. Queremos mostrar todos los tags donde el usuario tiene por lo menos un artÃculo. QuizÃĄ quieran extraer ese dato de la base de datos usando una consulta elaborada pero eso llevarÃa mucho tiempo. Primero hagamos un prototipo.
Antes de hacer las cosas de la manera adecuada lo que vamos a hacer es transformar un arreglo que contiene todos los artÃculos en un Set con todos los tags, para eso usaremos Array.reduce.
// Imaginen que estos objetos son mÃĄs complejos
const posts = [
{ tags: ["javascript", "discuss"] },
{ tags: ["javascript", "react", "vue-is-better"] },
{ tags: ["discuss"] },
{ tags: ["javascript"] },
];
function dangerously_add_tags(acc, post) {
for(let value of post.tags) {
acc.add(value);
}
return acc;
}
posts.reduce(dangerously_add_tags, new Set());
Este serÃa el resultado.
Set(4) [ "javascript", "discuss", "react", "vue-is-better" ]
Ahora piensen en el comportamiento de nuestro reducer. Tenemos un Set con tags que hace el papel de Accumulator y un objeto que representa un post como nuestro Value. PodrÃamos decir que se comporta de la siguiente manera.
(Set, Objeto) -> Set
Bueno, tÃĐcnicamente Objeto no puede ser cualquier objeto, debe tener una propiedad llamada tags. Asà que serÃa algo mÃĄs como esto.
(Set, ArtÃculo) -> Set
En fin, este es el patrÃģn del que les hablaba (A, B) -> A. La implementaciÃģn de dangerously_add_tags demanda que B sea un ArtÃculo. Pero para que esta funciÃģn sea un reducer debe ser capaz de retornar el mismo tipo de dato que recibiÃģ en el primer parÃĄmetro (Set), y eso lo logramos retornando acc.
Transformaciones
Probablemente han escuchado que pueden usar Array.reduce para suplantar otros mÃĐtodos del prototipo Array, pero aunque esto suene como un dato interesante no tiene mucha utilidad. ÂŋPor quÃĐ harÃan algo asÃ? No tiene sentido para mÃ. Sin embargo, aÚn puede resultar Útil si planean "fusionar" las caracterÃsticas de varios de esos mÃĐtodos en uno. ÂŋAlguna vez han querido filtrar y transformar un arreglo al mismo tiempo? Con .reduce eso es posible.
Vamos a reusar nuestra variable posts aquà tambiÃĐn.
const posts = [
{
category: "javascript",
tags: ["javascript", "discuss"]
},
{
category: "frameworks",
tags: ["javascript", "react", "vue-is-better"]
},
{
category: "watercooler",
tags: ["discuss"]
},
{
category: "functional programming",
tags: ["javascript"]
},
];
Esta vez lo que queremos hacer es filtrar los que tengan el tag discuss, y por cada uno que pase la prueba queremos extraer la categorÃa y capitalizar el valor. ÂŋCÃģmo harÃamos eso?
function capitalize(str) {
return str[0].toUpperCase() + str.slice(1);
}
function filter_map_posts(acc, post) {
// aquà estamos filtrando
if(post.tags.includes('discuss')) {
return acc.concat(
// esta es la transformaciÃģn
capitalize(post.category)
);
}
return acc;
}
posts.reduce(filter_map_posts, []);
Aquà tenemos nuestro resultado.
Array [ "Javascript", "Watercooler" ]
ÂŋPor quÃĐ funciona? Si revisan el comportamiento de filter_map_posts tenemos esto.
(Arreglo, ArtÃculo) -> Arreglo
CoordinaciÃģn
Si han indagado un poco en librerÃas enfocadas en el paradigma funcional hay una alta probabilidad de que se hayan topado con una funciÃģn llamada pipe. Con esta funciÃģn podemos combinar una cantidad arbitraria de funciones. Esta es la manera en la que se usa.
pipe(
una_funcion,
otra,
proceso_serio,
efectos_adelante,
);
La idea detrÃĄs de esto se centra en transportar el resultado de una funciÃģn hacia la siguiente en la lista. En efecto lo que hacemos aquà es coordinar llamadas de funciones. En este caso, el fragmento anterior es equivalente a esto:
function pipe(arg) {
return efectos_adelante(proceso_serio(otra(una_funcion(arg))));
}
Si se preguntan por quÃĐ les digo esto, es porque podemos implementar pipe usando .reduce. Si se fijan bien notarÃĄn que lo Único que hacemos en esa funciÃģn es aplicar funciones a un argumento. Eso es todo. No hay nada mÃĄs.
ÂŋY quÃĐ?
ÂĄEs una operaciÃģn binaria! Podemos transformar eso en una funciÃģn.
function apply(arg, fn) {
return fn(arg);
}
ÂŋY saben quÃĐ funciona bien con operaciones binarias? Nuestro amigo .reduce.
function pipe(...fns) {
return function(some_arg) {
return fns.reduce(apply, some_arg);
};
}
Lo primero que hacemos en pipe es recolectar la lista de funciones que usaremos y convertirla en un arreglo. El segundo paso es retornar una funciÃģn que desencadenarÃĄ las llamadas a las funciones en nuestro arreglo, tambiÃĐn en este paso obtenemos nuestro argumento inicial. Al final de eso, con todo en su lugar, .reduce se encarga del resto. Pueden probarlo ustedes mismos.
const post = {
category: "javascript",
tags: ["javascript", "discuss"]
}
function capitalize(str) {
return str[0].toUpperCase() + str.slice(1);
}
function get_prop(key) {
return function(obj) {
return obj[key];
}
}
function exclaim(str) {
return str + "!!";
}
const exciting_category = pipe(
get_prop("category"),
capitalize,
exclaim
);
exciting_category(post);
// => Javascript!!
Okey, okey. Ahora bien, ÂŋCÃģmo es que apply sigue el patrÃģn?
Ah, buena pregunta. Es algo raro pero aÚn podemos darle sentido. MÃrenlo de esta manera.
(Algo, FunciÃģn) -> Algo
Si tienen una unidad de lo que sea (literalmente cualquier cosa) y una funciÃģn, apply harÃĄ su trabajo. Pero tengan presente que aquà no hay garantÃa de que su funciÃģn no explote, esa serÃa responsabilidad de ustedes.
Cambios de estado a travÃĐs del tiempo
Este bonus track es para todos aquellos desarrolladores frontend allà afuera.
Si han pasado cualquier cantidad de tiempo investigando sobre librerÃas para manejar el estado de una aplicaciÃģn tal vez hayan escuchado de una cosa llamada redux. Esta librerÃa tiene un enfoque interesante porque espera que el usuario (el desarrollador) suministre un reducer que sea capaz de manejar los cambios en el estado de la aplicaciÃģn. A algunos les parece genial, a otros no. Pero bien sea que estÃĐn de acuerdo con esto o no, su enfoque tiene mucho sentido. DÃĐjenme mostrarles.
Empecemos con el reducer. En esta ocasiÃģn necesitamos uno con este comportamiento.
(Estado, AcciÃģn) -> Estado
Estado y AcciÃģn son objetos. Aquà no hay nada extravagante. La "forma" de nuestro Estado depende de la aplicaciÃģn en la que trabajamos, los desarrolladores pueden hacer lo que quieran con ÃĐl. La AcciÃģn por otro lado debe tener una propiedad type, y redux se asegura de esto.
Entonces, vamos a fingir que este es el estado de una aplicaciÃģn imaginaria en la que estamos trabajando.
const state = {
count: 40,
flag: false
};
Ah, sÃ. Un milagro de la ingenierÃa.
Ahora que sabemos cÃģmo luce el Estado, y tambiÃĐn sabemos quÃĐ necesita una AcciÃģn, podemos comenzar a escribir nuestro reducer.
function reducer(state, action) {
switch(action.type) {
case 'add':
return {
...state,
count: state.count + 1,
};
case 'subtract':
return {
...state,
count: state.count - 1,
};
case 'toggle_flag':
return {
...state,
flag: !state.flag,
};
default:
return state;
}
}
Aquà viene la parte curiosa: no necesitamos redux para probar nuestro reducer. Es un reducer genÃĐrico, bien podrÃamos usarlo con Array.reduce para ver quÃĐ puede hacer.
const actions = [
{ type: 'add' },
{ type: 'add' },
{ type: 'subtract' },
{ type: 'add' },
{ type: 'subtract' },
{ type: 'add' },
{ type: 'toggle_flag' }
];
actions.reduce(reducer, state);
actions.reduce deberÃa devolvernos otra "instancia" de nuestro estado. En nuestro caso, despuÃĐs de aplicar todas esas acciones, tendrÃamos el siguiente resultado.
{
count: 42,
flag: true
}
Y allà lo tienen, la funcionalidad principal de redux sin redux.
Vamos a dar un paso adelante en nuestro proceso e introduzcamos el concepto de tiempo. Para esto vamos a agregar una tienda "falsa" de redux. Bueno... la tienda serÃĄ "real" pero serÃĄ una imitaciÃģn barata. Empecemos.
function Store(reducer, state) {
let _listener = null;
const get_state = function() {
return state;
};
const subscribe = function(listener) {
_listener = listener;
};
const dispatch = function(action) {
state = reducer(state, action);
_listener && _listener();
return action;
};
return { get_state, dispatch, subscribe };
}
ÂŋTodo bien? ÂŋSaben lo que ocurre ahÃ? La parte que realmente nos interesa es la del dispatch. Esto de aquÃ.
const dispatch = function(action) {
state = reducer(state, action);
_listener && _listener();
return action;
};
Esta funciÃģn se encarga de reemplazar el Estado actual. Como mencionÃĐ antes, el reducer se encarga de la lÃģgica que dice cÃģmo actualizar el Estado. La tienda (Store) se encarga de la lÃģgica que dice cuando debe actualizarse. Suficiente palabrerÃa, vamos a probarlo.
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const store = Store(reducer, state);
store.subscribe(function() {
console.log(store.get_state());
});
(async function() {
store.dispatch({ type: 'add' });
await delay(500);
store.dispatch({ type: 'add' });
await delay(500);
store.dispatch({ type: 'subtract' });
await delay(700);
store.dispatch({ type: 'add' });
await delay(400);
store.dispatch({ type: 'subtract' });
await delay(800);
store.dispatch({ type: 'add' });
await delay(100);
store.dispatch({ type: 'toggle_flag' });
})();
Si ejecutan eso notarÃĄn cÃģmo los mensajes aparecen en pantalla (o la cÃģnsola del navegador) con una pequeÃąa demora entre cada uno.
- { count: 41, flag: false }
- { count: 42, flag: false }
- { count: 41, flag: false }
- { count: 42, flag: false }
- { count: 41, flag: false }
- { count: 42, flag: false }
- { count: 42, flag: true }
ÂŋSe dieron cuenta que el resultado final es el mismo que nos dio Array.reduce? ÂŋNo es genial?
Si quieren jugar con el verdadero redux aquà les dejo un ejemplo en codepen.
ConclusiÃģn
Espero que en este punto los reducers no parezcan tan misteriosos y aterradores. SÃģlo recuerden que se trata de una funciÃģn con este comportamiento.
(A, B) -> A
Eso es todo. No hay ninguna magia extraÃąa detrÃĄs de eso. Si pueden hacer que una funciÃģn tenga esas caracterÃsticas pueden estar seguros que funcionarÃĄ de maravilla con cualquier cosa que actue como .reduce.
Fuentes
Gracias por su tiempo. Si este artÃculo les pareciÃģ Útil y quieren apoyar mis esfuerzos para crear mÃĄs contenido pueden dejar una propina en ko-fi.com/vonheikemen.

Top comments (4)
Que interesante esto sobre funcionalidad de Redux sin Redux. Este mÃĐtodo
reduceme cae bien no sÃģlo por ser bien Útil sino porque recuerda la funcionalidad de los "folds" en Haskell y Functional Programming FTW!Era un dato curioso que siempre quise mencionar. Ahora que en
reacttiene un hook llamadouseReducerese dato puede resultar Útil incluso si la gente ya no usaredux.No sÃĐ mucho de haskell pero segÚn la especificaciÃģn Fantasy Land, definitivamente hay una relaciÃģn.
Nunca habÃa visto eso de Fantasy Land aunque se ve cool. Pero si
reduceen JS es bÃĄsicamente unfoldlen Haskell osea coger un arreglo y "doblarlo" de izquierda a derecha.TambiÃĐn estÃĄ
foldrque hace lo mismo pero al revÃĐs.TambiÃĐn existe el mÃĐtodo .reduceRight en los arreglos.