Vamos a hablar del elefante rosa en el prototipo Array, me refiero al a veces odiado mÃĐtodo reduce pero no vamos a discutir sobre si esta funciÃģn es buena o mala. Vamos a descubrir quÃĐ es lo que hace internamente, luego intentaremos descubrir las situaciones en las que puede ser una soluciÃģn efectiva.
Para asegurarnos de entender su funcionamiento vamos a empezar implementando nuestra versiÃģn.
ÂŋCÃģmo funciona?
reduce es una funciÃģn que toma una lista de valores y la transforma en otra cosa. La palabra clave aquà es transformaciÃģn. Esta transformaciÃģn la determina el "usuario" de nuestra funciÃģn, son ellos los que deciden quÃĐ pasarÃĄ ÂŋQuÃĐ quiere decir eso? Significa que aparte del arreglo que vamos a procesar necesitamos aceptar una funciÃģn (un callback) como parÃĄmetro. Asà que la firma de la funciÃģn serÃa esta.
function reduce(arr, callback) {
// cÃģdigo...
}
Ya tenemos algunos valores, y ahora ÂŋQuÃĐ hacemos con ellos? Sabemos que los mÃĐtodos del prototipo Array aplican una funciÃģn a cada uno de sus elementos. Hagamos eso.
function reduce(arr, callback) {
for(const valor of arr) {
callback(valor);
}
}
TodavÃa no hace lo que queremos pero se acerca. Ahora falta el ingrediente secreto, el acumulador. Esta serÃĄ una variable que crearemos para recordar el estado actual de nuestra transformaciÃģn. Cada vez que apliquemos la funciÃģn callback a un valor guardamos el resultado en el acumulador. Como bono extra, antes de guardar el nuevo estado en el acumulador le pasamos a callback el estado actual para que nuestro "usuario" no tenga que hacer ningÚn esfuerzo extra.
function reduce(arr, callback) {
+ let estado;
for(const valor of arr) {
- callback(valor);
+ estado = callback(estado, valor);
}
-
+ return estado;
}
Recuerden bien esas lÃneas que estÃĄn verde. Por muy complicado que se vea reduce en el exterior, sin importar cuantos trucos raros vean por ahÃ, esas tres lÃneas son lo Único que importa.
Aunque no sea una replica exacta de Array.reduce serÃĄ suficiente para nuestros propÃģsitos. Vamos a probarla.
const array1 = [1, 2, 3, 4];
const callback = (estado, valor) => {
if(estado == null) {
return valor;
}
return estado + valor;
};
// 1 + 2 + 3 + 4
reduce(array1, callback);
// valor esperado: 10
ÂŋVen ese if? EstÃĄ ahà porque en la primera iteraciÃģn estado no tiene un valor, eso parece innecesario. Nosotros como autores de reduce podemos ayudar a reducir la cantidad de cÃģdigo que necesita callback. Al disminuir la carga de responsabilidad que necesita callback podemos hacer que reduce sea mucho mÃĄs flexible. Lo que haremos serÃĄ tomar el primer valor del arreglo y ese se convertirÃĄ en el estado para nuestra primera iteraciÃģn.
function reduce(arr, callback) {
- let estado;
- for(const valor of arr) {
+ let estado = arr[0];
+ let resto = arr.slice(1);
+ for(const valor of resto) {
estado = callback(estado, valor);
}
return estado;
}
Vamos otra vez.
const array1 = [1, 2, 3, 4];
const callback = (estado, valor) => {
return estado + valor;
};
// 1 + 2 + 3 + 4
reduce(array1, callback);
// valor esperado: 10
Si aÚn les cuesta un poco descifrar lo que estÃĄ pasando, puedo ayudarles con eso. Si sacamos callback de la ecuaciÃģn esto es lo que sucede.
function reduce(arr) {
let estado = arr[0];
let resto = arr.slice(1);
for(const valor of resto) {
estado = estado + valor;
}
return estado;
}
ÂŋRecuerdan las tres lÃneas verdes?
function reduce(arr) {
+ let estado = arr[0];
let resto = arr.slice(1);
for(const valor of resto) {
+ estado = estado + valor;
}
-
+ return estado;
}
ÂŋLo notaron? Eso es todo lo que necesitan recordar. BÃĄsicamente,reduce nos da la habilidad de transformar una operaciÃģn que actÚa sobre dos valores a una que actÚa sobre una cantidad variada.
ÂŋCuando es Útil?
reduce es una de esas funciones que pueden utilizarse en muchas ocasiones pero no en todas es la mejor soluciÃģn. Ahora que sabemos cÃģmo funciona veamos en quÃĐ tipo de situaciones puede ser la mejor opciÃģn.
Un caso ideal
El ejemplo anterior ya deberÃa darles una pista. Nuestra funciÃģn es mÃĄs efectiva cuando seguimos ciertos patrones. Pensemos un momento en lo que hace callback en nuestro ejemplo. Sabemos que necesita dos nÚmeros, ejecuta una operaciÃģn matemÃĄtica y nos devuelve otro nÚmero. Entonces hace esto.
NÚmero + NÚmero -> NÚmero
Bien, pero si damos un paso atrÃĄs y pensamos en tÃĐrminos mÃĄs generales lo que tenemos es esto.
TipoA + TipoA -> TipoA
Hay dos valores del mismo tipo (TipoA) y una operaciÃģn (el signo +) que nos devuelve otro valor del mismo tipo (TipoA). Cuando lo vemos de esa manera podemos darnos cuenta de un patrÃģn que puede ser Útil mÃĄs allÃĄ de las operaciones matemÃĄticas. Hagamos otro ejemplo con nÚmeros pero esta vez lo que haremos serÃĄ una comparaciÃģn.
function max(un_numero, otro_numero) {
if(un_numero > otro_numero) {
return un_numero;
} else {
return otro_numero;
}
}
max es una operaciÃģn que actÚa sobre dos nÚmeros, los compara y devuelve el mayor. Es muy general y con una capacidad limitada. Si volvemos a pensar en lo abstracto vemos ese patrÃģn otra vez.
TipoA + TipoA -> TipoA
O si somos mÃĄs especÃficos.
NÚmero + NÚmero -> NÚmero
Ya saben lo que significa, podemos usar reduce para ampliar su capacidad.
const array2 = [40, 41, 42, 39, 38];
// 40 > 41 > 42 > 39 > 38
reduce(array2, max);
// valor esperado: 42
Resulta que el patrÃģn que hemos estado siguiendo para crear el callback que necesita reduce tiene un nombre en el paradigma funcional, a este lo llaman Semigrupo. En cualquier ocasiÃģn en la tengan dos valores de un mismo tipo y puedan combinarlos para crear otra instancia, estÃĄn en la presencia de un semigrupo. En otras palabras, dos valores + forma de combinarlos = Semigrupo.
Una manera de probar que tienen una operaciÃģn que sigue las reglas de un semigrupo es asegurarse de que la funciÃģn cumple con la propiedad asociativa. Nuestra funciÃģn max por ejemplo.
const max_1 = max(max(40, 42), 41); // => 42
const max_2 = max(40, max(42, 41)); // => 42
max_1 === max_2
// valor esperado: true
ÂŋVen? Ejecutarla con el tipo de dato adecuado en diferente orden no afecta su resultado. Esto nos da la garantÃa de que funcionarÃĄ si la combinamos con reduce y un arreglo de nÚmeros.
ÂŋPero podrÃamos aplicar estas reglas a una estructura mÃĄs compleja? Claro que sÃ. En javascript ya tenemos un par que las cumplen. Piensen en los arreglos, en el prototipo Array tenemos el mÃĐtodo concat, este nos permite mezclar dos arreglos y crear uno nuevo con los elementos de ambos.
function concat(uno, otro) {
return uno.concat(otro);
}
Con esto tenemos que.
Array + Array -> Array
Okey, el segundo parÃĄmetro de concat no tiene que ser un arreglo pero ignoraremos eso por el momento. Entonces si combinamos concat con reduce.
const array3 = [[40, 41], [42], [39, 38]];
// [40, 41] + [42] + [39, 38]
reduce(array3, concat);
// valor esperado: [40, 41, 42, 39, 38]
Ahora si quisieramos podrÃamos crear una funciÃģn que "aplana" un nivel de un arreglo multidimensional, ÂŋNo es genial? Y al igual que con los nÚmeros, con los arreglos no tenemos porque limitarnos a operaciones que nos provee javascript. Si tenemos alguna funciÃģn auxiliar que trabaje con dos arreglos y cumpla con la propiedad asociativa podemos combinarla con reduce.
Digamos que tenemos una funciÃģn que une los elementos Únicos de dos arreglos.
function union(uno, otro) {
const set = new Set([...uno, ...otro]);
return Array.from(set);
}
Bien, tenemos una funciÃģn que trabaja con dos valores de un mismo tipo ahora veamos si cumple con la propiedad asociativa.
const union_1 = union(union([40, 41], [40, 41, 42]), [39]);
const union_2 = union([40, 41], union([40, 41, 42], [39]));
union_1.join(',') == union_2.join(',');
// valor esperado: true
SÃ cumple con las reglas, eso quiere decir que es posible procesar una cantidad variada de arreglos si usamos reduce.
const array4 = [
['hello'],
['hello', 'awesome'],
['world', '!'],
['!!', 'world']
];
reduce(array4, union);
// valor esperado: [ "hello", "awesome", "world", "!", "!!" ]
Algo de resistencia
HabrÃĄn notado que en todos los ejemplos nuestros arreglos de datos son todos del tipo correcto, esto no siempre es asà en el "mundo real". Podemos encontrarnos situaciones en las que el primer elemento de un arreglo no es un dato vÃĄlido para nuestra operaciÃģn.
Imaginemos que queremos usar concat otra vez pero el arreglo que tenemos para procesar es el siguiente.
const array5 = [40, 41, [42], [39, 38]];
Si intentamos usar reduce.
reduce(array5, concat);
Obtenemos esto.
TypeError: uno.concat is not a function
Esto ocurre porque en la primera iteraciÃģn el valor de uno es el nÚmero 40, el cual no posee un mÃĐtodo concat. ÂŋQuÃĐ debemos hacer? Por lo general se considera una buena prÃĄctica utilizar un valor inicial fijo para evitar este tipo de errores. Pero tenemos un problema, nuestro reduce no acepta un valor inicial, asà que deberÃamos corregir eso.
- function reduce(arr, callback) {
- let estado = arr[0];
- let resto = arr.slice(1);
+ function reduce(arr, ...args) {
+ if(args.length === 1) {
+ var [callback] = args;
+ var estado = arr[0];
+ var resto = arr.slice(1);
+ } else if(args.length >= 2) {
+ var [estado, callback] = args;
+ var resto = arr;
+ }
for(const valor of resto) {
estado = callback(estado, valor);
}
return estado;
}
Ahora para evitar el error anterior lo que haremos serÃĄ pasarle a reduce un arreglo vacÃo como valor inicial.
reduce(array5, [], concat);
// valor esperado: [ 40, 41, 42, 39, 38 ]
Ya no hay error y pudimos obtener el arreglo que querÃamos. Pero fÃjense en una cosa, el arreglo vacÃo no sÃģlo logrÃģ evitar el error sino que tambiÃĐn dejÃģ intacto el resultado de la operaciÃģn. Al igual que con los nÚmeros, con los arreglos tenemos la nociÃģn de un elemento vacÃo que podemos usar en nuestras operaciones sin causar un error en nuestro programa.
El arreglo vacÃo puede considerarse como un elemento identidad, un valor neutro que al aplicarlo a una operaciÃģn no tiene ningÚn efecto en el resultado final. Adivinen quÃĐ, este comportamiento tambiÃĐn tiene un nombre en el paradigma funcional, se le conoce como Monoid. Cuando tenemos un semigrupo con un elemento identidad estamos en presencia de un monoid. Entonces, semigrupo + elemento identidad = Monoid.
Podemos probar que los arreglos siguen las reglas de un monoid para nuestras operaciones.
// Concat
const concat_1 = concat([], ['hello']) // => ["hello"]
const concat_2 = concat(['hello'], []) // => ["hello"]
concat_1.join(',') == concat_2.join(',');
// valor esperado: true
// Union
const union_3 = union([], ['hello']); // => ["hello"]
const union_4 = union(['hello'], []); // => ["hello"]
union_3.join(',') == union_4.join(',');
// valor esperado: true
ÂŋPor quÃĐ es importante? Piensen en esto: ÂŋCuantas veces han tenido que escribir un if para resguardar una operaciÃģn de un valor null o undefined? Si podemos representar un "valor vacÃo" de una manera mÃĄs segura podemos eliminar toda una categorÃa de errores en nuestros programas.
Otra situaciÃģn donde los monoids son Útiles es cuando queremos ejecutar una operaciÃģn "insegura" sobre un valor. PodrÃamos aplicar esa operaciÃģn sobre una referencia a un valor vacÃo y asà dejar el resto de los elementos intactos.
Imaginen que tienen fragmentos de informaciÃģn esparcidos en varios objectos y queremos unirlos.
const array6 = [
{name: 'Harold'},
{lastname: 'Cooper'},
{state: 'wrong'}
];
Normalmente usarÃan la sintaxis de extensiÃģn para mezclar todo eso, pero digamos que vivimos en un mundo donde eso no es posible. No teman, tenemos una funciÃģn que puede hacer el trabajo.
Object.assign;
Si lo piensan Object.assign tambiÃĐn sigue el patrÃģn.
TipoA + TipoA -> TipoA
Si le pasamos dos objetos nos devuelve un nuevo objecto. Pero hay algo que deben saber, Object.assign modifica el objeto que le pasamos como primer parÃĄmetro. Entonces si hacemos esto.
reduce(array6, Object.assign);
// Valor esperado: { "name": "Harold", "lastname": "Cooper", "state": "wrong" }
ParecerÃa que todo estÃĄ bien, pero no es asÃ. Si revisan array6[0] verÃĄn que ha cambiado, definitivamente no quieren eso. Por suerte para nosotros los objetos en javascript se comportan como monoids, asà que podemos usar un "valor vacÃo". Entonces la manera correcta de usar reduce en este caso serÃa esta.
reduce(array6, {}, Object.assign);
// Valor esperado: { "name": "Harold", "lastname": "Cooper", "state": "wrong" }
array6
// Valor esperado: [ { "name": "Harold" }, { "lastname": "Cooper" }, { "state": "wrong" } ]
Podemos decir que cuando trabajamos con un arreglo de estructuras que siguen las reglas de los monoid podemos estar seguros que reduce serÃĄ una buena opciÃģn para procesarlo.
MÃĄs allÃĄ de los arreglos
Si nosotros pudimos implementar una versiÃģn de reduce para los arreglos entonces no serÃa del todo extraÃąo pensar que otras personas hayan incorporado algo similar a otras estructuras. Saber cÃģmo funciona reduce puede ser muy Útil si utilizan una librerÃa que tenga un mÃĐtodo parecido.
Por ejemplo, la librerÃa mithril-stream tiene un mÃĐtodo llamado scan que tiene la siguiente forma.
Stream.scan(fn, accumulator, stream)
Esa variable fn debe ser una funciÃģn que debe tener la siguiente firma.
(accumulator, value) -> result | SKIP
ÂŋReconocen eso? Espero que sÃ. Son los mismos requerimientos de reduce. ÂŋPero quÃĐ hace esa funciÃģn? Bueno, ejecuta la funciÃģn fn cuando la fuente (stream) produce un nuevo dato. Cuando la funciÃģn fn es ejecutada recibe como parÃĄmetro el estado actual del acumulador y el nuevo dato producido, luego el resultado retornado por fn se convierte en el nuevo estado del acumulador. ÂŋLes suena familiar ese comportamiento?
Pueden probar el mÃĐtodo scan con nuestra funciÃģn union y ver cÃģmo se comporta.
import Stream from 'https://cdn.pika.dev/mithril-stream@^2.0.0';
function union(one, another) {
const set = new Set([...one, ...another]);
return Array.from(set);
}
const list = Stream(['node', 'js']);
const state = Stream.scan(union, [], list);
state.map(console.log);
list(['node']);
list(['js', 'deno']);
list(['node', 'javascript']);
DeberÃan observar cÃģmo la lista sÃģlo agrega elementos que no han sido agregados antes.
Pueden ver en acciÃģn una versiÃģn modificada de ese fragmento en codepen.
ÂŋVieron? nuestro conocimiento de reduce (y tal vez algo de semigrupos y monoids) nos puede ayudar a crear funciones auxiliares que podemos reusar con diferentes estructuras. ÂŋNo es genial?
ConclusiÃģn
Aunque no mencionÃĐ todas las cosas que pueden hacer con reduce ahora tienen las herramientas para poder identificar los casos en lo que puede ser utilizado efectivamente, incluso si no estÃĄn seguros pueden hacer las pruebas necesarias para garantizar que la operaciÃģn que quieren ejecutar tiene las caracterÃsticas adecuadas.
Fuentes
- Practical Category Theory: Monoids (video)
- Funcadelic.js
- Functional JavaScript: How to use array reduce for more than just numbers
- Array.prototype.reduce (MDN)
- Fantasy Land
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)
Tremendo artÃculo. Siempre vi
Array.reducecomo una forma de sumar nÚmeros en una lista xDEl dolor de cabeza de muchos :D
De nuevo, excelente artÃculo Heiker.
No estabas equivocado.
Gracias.
Buen trabajo
gracias.