Los métodos de iteración de matriz son similares a "drogas iniciales" (por supuesto, no son drogas; y no estoy diciendo que las drogas sean buenas; son solo una figura retórica). Debido a ellos, muchos "se sientan" en la programación funcional. La cosa es que son increíblemente convenientes. Además, la mayoría de estos métodos son muy fáciles de entender. Métodos como
.map()
y
.filter()
aceptan solo un argumento de devolución de llamada y le permiten resolver problemas simples. Pero existe la sensación de que el método
.reduce()
causa algunas dificultades para muchos. Comprenderlo es un poco más difícil.

Ya
escribí sobre por qué creo que
.reduce()
crea muchos problemas. Esto se debe en parte al hecho de que muchos manuales demuestran el uso de
.reduce()
solo cuando se manejan números. Por lo tanto, escribí sobre cuántas tareas que no implican operaciones aritméticas se pueden resolver usando
.reduce()
. Pero, ¿qué pasa si absolutamente necesitas trabajar con números?
Un uso típico de
.reduce()
parece un cálculo de la media aritmética de los elementos de una matriz. A primera vista, parece que no hay nada especial en esta tarea. Pero ella no es tan simple. El hecho es que antes de calcular el promedio, debe encontrar los siguientes indicadores:
- La cantidad total de valores de elementos de matriz.
- La longitud de la matriz.
Descubrir todo esto es bastante simple. Y los promedios informáticos para matrices numéricas tampoco es una operación fácil. Aquí hay un ejemplo elemental:
function average(nums) { return nums.reduce((a, b) => (a + b)) / nums.length; }
Como puede ver, no hay incomprensiones especiales aquí. Pero la tarea se vuelve más difícil si tiene que trabajar con estructuras de datos más complejas. ¿Qué pasa si tenemos una matriz de objetos? ¿Qué pasa si algunos objetos de esta matriz necesitan ser filtrados? ¿Qué hacer si necesita extraer ciertos valores numéricos de los objetos? En esta situación, calcular el valor promedio de los elementos de la matriz ya es una tarea un poco más complicada.
Para lidiar con esto, resolveremos el problema de capacitación (se basa en
esta tarea con FreeCodeCamp). Lo resolveremos de cinco maneras diferentes. Cada uno de ellos tiene sus propias ventajas y desventajas. Un análisis de estos cinco enfoques para resolver este problema mostrará cuán flexible puede ser JavaScript. Y espero que el análisis de las soluciones le permita reflexionar sobre cómo usar
.reduce()
en proyectos reales.
Descripción general de la tarea
Supongamos que tenemos una serie de objetos que describen expresiones de argot victorianas. Debe filtrar aquellas expresiones que no se encuentran en Google Books (la propiedad
found
de los objetos correspondientes es
false
) y encontrar una calificación promedio para la popularidad de las expresiones. Así es como se verían dichos datos (tomados
de aquí ):
const victorianSlang = [ term: 'doing the bear', found: true, popularity: 108, }, term: 'katterzem', found: false, popularity: null, }, term: 'bone shaker', found: true, popularity: 609, }, term: 'smothering a parrot', found: false, popularity: null, }, term: 'damfino', found: true, popularity: 232, }, term: 'rain napper', found: false, popularity: null, }, term: 'donkey's breakfast', found: true, popularity: 787, }, term: 'rational costume', found: true, popularity: 513, }, term: 'mind the grease', found: true, popularity: 154, }, ];
Considere 5 formas de encontrar el valor promedio de evaluar la popularidad de las expresiones de esta matriz.
1. Resolver un problema sin usar .reduce () (bucle imperativo)
En nuestro primer enfoque para resolver el problema, no se usará el método
.reduce()
. Si no ha encontrado métodos para iterar matrices antes, entonces espero que analizar este ejemplo le aclare un poco la situación.
let popularitySum = 0; let itemsFound = 0; const len = victorianSlang.length; let item = null; for (let i = 0; i < len; i++) { item = victorianSlang[i]; if (item.found) { popularitySum = item.popularity + popularitySum; itemsFound = itemsFound + 1; } const averagePopularity = popularitySum / itemsFound; console.log("Average popularity:", averagePopularity);
Si está familiarizado con JavaScript, comprenderá fácilmente este ejemplo. De hecho, aquí sucede lo siguiente:
- Inicializamos las variables
popularitySum
y itemsFound
. La primera variable, popularitySum
, almacena la calificación general de popularidad de las expresiones. Y la segunda variable, itemsFound
, (eso es una sorpresa) almacena el número de expresiones encontradas. - Luego, inicializamos la constante
len
y el item
variable, que nos son útiles al atravesar la matriz. - En un bucle
for
, el contador i
incrementa hasta que su valor alcanza el valor de índice del último elemento de la matriz. - Dentro del bucle, tomamos el elemento de la matriz que queremos explorar. Accedemos al elemento utilizando la construcción
victorianSlang[i]
. - Luego descubrimos si esta expresión se encuentra en la colección de libros.
- Si se produce una expresión en los libros, tomamos el valor de su calificación de popularidad y lo agregamos al valor de la
popularitySum
variable. - Al mismo tiempo, también aumentamos el contador de las expresiones encontradas:
itemsFound
. - Y, por último, encontramos el promedio dividiendo la
popularitySum
itemsFound
por elementos itemsFound
.
Entonces, hicimos frente a la tarea. Quizás nuestra decisión no fue particularmente hermosa, pero hace su trabajo. El uso de métodos para iterar a través de matrices lo hará un poco más limpio. Echemos un vistazo a si tenemos éxito, y la verdad es, "limpiar" esta decisión.
2. Solución simple # 1: .filter (), .map () y encontrar la cantidad usando .reduce ()
Antes del primer intento de usar los métodos de matrices para resolver el problema, lo dividimos en partes pequeñas. A saber, esto es lo que debemos hacer:
- Seleccione objetos que representen expresiones que están en la colección de Google Books. Aquí puede usar el método
.filter()
. - Extraer de los objetos la evaluación de la popularidad de las expresiones. Para resolver esta subtarea, el método
.map()
es adecuado. - Calcule la suma de las calificaciones. Aquí podemos recurrir a la ayuda de nuestro viejo amigo
.reduce()
. - Y finalmente, encuentre el valor promedio de las estimaciones.
Así es como se ve en el código:
Observe la función
addScore
y la línea donde
.reduce()
llama
.reduce()
. Tenga en cuenta que
addScore
acepta dos parámetros. El primero,
runningTotal
, se conoce como batería. Almacena la suma de los valores. Su valor cambia cada vez que iteramos sobre la matriz y ejecutamos la
return
. El segundo parámetro, la
popularity
, es un elemento separado de la matriz que estamos procesando. Al principio de
addScore
sobre la matriz, la
return
addScore
en
addScore
nunca se ha ejecutado. Esto significa que
runningTotal
no se ha configurado automáticamente. Por lo tanto, al llamar a
.reduce()
, pasamos a este método el valor que debe escribirse en
runningTotal
desde el principio. Este es el segundo parámetro pasado a
.reduce()
.
Entonces, aplicamos los métodos de iterar matrices para resolver el problema. La nueva versión de la solución resultó ser mucho más limpia que la anterior. En otras palabras, la decisión resultó ser más declarativa. No le decimos a JavaScript exactamente cómo ejecutar el bucle; no seguimos los índices de los elementos de las matrices. En cambio, declaramos funciones auxiliares simples de pequeño tamaño y las combinamos. Todo el trabajo duro se hace por nosotros mediante los métodos de matriz
.filter()
,
.map()
y
.reduce()
. Este enfoque para resolver tales problemas es más expresivo. Estos métodos de matriz son mucho más completos de lo que puede hacer el ciclo, nos informan sobre la intención establecida en el código.
3. Solución fácil # 2: uso de múltiples baterías
En la versión anterior de la solución, creamos un montón de variables intermedias. Por ejemplo,
foundSlangTerms
y
popularityScores
. En nuestro caso, tal solución es bastante aceptable. Pero, ¿qué pasa si nos fijamos un objetivo más complejo con respecto al diseño del código? Sería bueno si pudiéramos usar el patrón de diseño de
interfaz fluido en el programa. Con este enfoque, podríamos encadenar las llamadas de todas las funciones y ser capaces de prescindir de variables intermedias. Sin embargo, un problema nos espera aquí. Tenga en cuenta que necesitamos obtener el valor de
popularityScores.length
. Si vamos a encadenar todo, entonces necesitamos otra forma de encontrar el número de elementos en la matriz. El número de elementos en la matriz juega el papel de un divisor en el cálculo del valor promedio. Veamos si podemos cambiar el enfoque para resolver el problema para que todo se pueda hacer combinando llamadas a métodos en una cadena. Haremos esto mediante el seguimiento de dos valores al iterar sobre los elementos de la matriz, es decir, utilizando la "batería doble".
Aquí, para trabajar con dos valores, utilizamos el objeto en la función reductora. Cada pasada a través de la matriz realizada con
addScrores
, actualizamos el valor total de la clasificación de popularidad y el número de elementos. Es importante tener en cuenta que estos dos valores están representados como un solo objeto. Con este enfoque, podemos "engañar" al sistema y almacenar dos entidades dentro del mismo valor de retorno.
La función
addScrores
ser un poco más complicada que la función con el mismo nombre en el ejemplo anterior. Pero ahora resulta que podemos usar una sola cadena de llamadas a métodos para realizar todas las operaciones con la matriz. Como resultado del procesamiento de la matriz, obtenemos un objeto de
popularityInfo
que almacena todo lo que necesita para encontrar el promedio. Esto hace que la cadena de llamadas sea ordenada y simple.
Si siente el deseo de mejorar este código, puede experimentar con él. Por ejemplo, puede rehacerlo para deshacerse de muchas variables intermedias. Incluso puede intentar poner este código en una línea.
4. Composición de funciones sin usar notación de puntos
Si es nuevo en la programación funcional, o si le parece que la programación funcional es demasiado complicada, puede omitir esta sección. Analizarlo te beneficiará si ya estás familiarizado con
curry()
y
compose()
. Si desea profundizar en este tema, eche un vistazo a
este material sobre programación funcional en JavaScript y, en particular, en la
tercera parte de la serie en la que está incluido.
Somos programadores que adoptamos un enfoque funcional. Esto significa que nos esforzamos por construir funciones complejas a partir de otras funciones, pequeñas y simples. Hasta ahora, en el curso de considerar varias opciones para resolver el problema, hemos reducido el número de variables intermedias. Como resultado, el código de la solución se volvió más simple y fácil. Pero, ¿qué pasa si esta idea se lleva al extremo? ¿Qué pasa si intenta deshacerse de todas las variables intermedias? ¿E incluso tratar de escapar de algunos parámetros?
Puede crear una función para calcular el promedio usando solo la función
compose()
, sin usar variables. Llamamos a esto "programación sin el uso de notación de grano fino" o "programación implícita". Para escribir tales programas, necesitará muchas funciones auxiliares.
A veces, tal código sorprende a la gente. Esto se debe al hecho de que este enfoque es muy diferente del generalmente aceptado. Pero descubrí que escribir código al estilo de la programación implícita es una de las formas más rápidas de comprender la esencia de la programación funcional. Por lo tanto, puedo aconsejarle que pruebe esta técnica en algún proyecto personal. Pero quiero decir que quizás no debería escribir en el estilo de programación implícita el código que otras personas tienen que leer.
Entonces, volvamos a nuestra tarea de construir un sistema para calcular promedios. En aras de ahorrar espacio, pasaremos aquí al uso de las funciones de flecha. Por lo general, como regla, es mejor usar funciones con nombre.
Aquí hay un buen artículo sobre este tema. Esto le permite obtener mejores resultados de seguimiento de pila en caso de errores.
Si todo este código te parece una tontería completa, no te preocupes por eso. Lo incluí aquí como un ejercicio intelectual, y no para molestarte.
En este caso, el trabajo principal está en la función
compose()
. Si lee su contenido de abajo hacia arriba, resulta que los cálculos comienzan filtrando la matriz por la propiedad de sus elementos
found
. Luego recuperamos la propiedad del elemento de
popularity
usando
map()
. Después de eso usamos el llamado "
combinador de mirlo ". Esta entidad se representa como una función
B1
, que se utiliza para realizar dos pases de cálculos en un conjunto de datos de entrada. Para comprender mejor esto, eche un vistazo a estos ejemplos:
Nuevamente, si no comprende nada otra vez, no se preocupe. Esto es solo una demostración de que JavaScript se puede escribir de maneras muy diferentes. De estas características, esta es la belleza de este lenguaje.
5. Resolver el problema de una sola vez con el cálculo del valor promedio acumulado
Todas las construcciones de software anteriores hacen un buen trabajo para resolver nuestro problema (incluido el ciclo imperativo). Aquellos que usan el método
.reduce()
tienen algo en común. Se basan en dividir el problema en pequeños fragmentos. Estos fragmentos se ensamblan de varias maneras. Al analizar estas soluciones, puede notar que en ellas damos la vuelta a la matriz tres veces. Existe la sensación de que es ineficaz. Sería bueno si hubiera una manera de procesar la matriz y devolver el resultado en una sola pasada. Este método existe, pero su aplicación requerirá recurrir a las matemáticas.
Para calcular el valor promedio de los elementos de la matriz en una sola pasada, necesitamos un nuevo método. Necesita encontrar una manera de calcular el promedio utilizando el promedio calculado previamente y el nuevo valor. Buscamos este método usando álgebra.
El valor promedio de
n
números se puede encontrar usando esta fórmula:
Para encontrar los números promedio
n + 1
, la misma fórmula funcionará, pero en una entrada diferente:
Esta fórmula es la misma que esta:
Y lo mismo que esto:
Si convierte esto un poco, obtendrá lo siguiente:
Si no ve el punto en todo esto, entonces está bien. El resultado de todas estas transformaciones es que con la ayuda de la última fórmula podemos calcular el valor promedio durante un solo recorrido de la matriz. Para hacer esto, debe conocer el valor del elemento actual, el valor promedio calculado en el paso anterior y el número de elementos. Además, la mayoría de los cálculos se pueden realizar en la función reductora:
Gracias a este enfoque, el valor necesario se puede encontrar sin pasar por la matriz solo una vez. Otros enfoques usan una pasada para filtrar la matriz, otra para extraer los datos necesarios de ella y otra para encontrar la suma de los valores de los elementos. Aquí, todo encaja en un solo paso a través de la matriz.
Tenga en cuenta que esto no necesariamente hace que los cálculos sean más eficientes. Con este enfoque, se deben hacer más cálculos. Cuando llega cada nuevo valor, realizamos las operaciones de multiplicación y división, haciendo esto para mantener el valor promedio actual en el estado actual. En otras soluciones a este problema, dividimos un número en otro solo una vez, al final del programa. Pero este enfoque es mucho más eficiente en términos de uso de memoria. Aquí no se utilizan matrices intermedias, como resultado tenemos que almacenar en la memoria solo un objeto con dos valores.
. . , . . , , .
?
? , . , - . , , , . , . , . , , .
, - , . , ? . . — .
:
.reduce()
..filter()
.map()
, — .reduce()
.- , .
- .
- .
, -, ? — . - — , :
- , . — .
- , , — .
- , , — , .
! JavaScript-?