JavaScript funcional: cinco formas de encontrar la media aritmética de los elementos de la matriz y el método .reduce ()

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:

  1. La cantidad total de valores de elementos de matriz.
  2. 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:

  1. 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.
  2. Luego, inicializamos la constante len y el item variable, que nos son útiles al atravesar la matriz.
  3. En un bucle for , el contador i incrementa hasta que su valor alcanza el valor de índice del último elemento de la matriz.
  4. Dentro del bucle, tomamos el elemento de la matriz que queremos explorar. Accedemos al elemento utilizando la construcción victorianSlang[i] .
  5. Luego descubrimos si esta expresión se encuentra en la colección de libros.
  6. 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.
  7. Al mismo tiempo, también aumentamos el contador de las expresiones encontradas: itemsFound .
  8. 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:

  1. Seleccione objetos que representen expresiones que están en la colección de Google Books. Aquí puede usar el método .filter() .
  2. Extraer de los objetos la evaluación de la popularidad de las expresiones. Para resolver esta subtarea, el método .map() es adecuado.
  3. Calcule la suma de las calificaciones. Aquí podemos recurrir a la ayuda de nuestro viejo amigo .reduce() .
  4. Y finalmente, encuentre el valor promedio de las estimaciones.

Así es como se ve en el código:

 //   // ---------------------------------------------------------------------------- function isFound(item) {    return item.found; }; function getPopularity(item) {    return item.popularity; } function addScores(runningTotal, popularity) {    return runningTotal + popularity; } //  // ---------------------------------------------------------------------------- //  ,      . const foundSlangTerms = victorianSlang.filter(isFound); //   ,   . const popularityScores = foundSlangTerms.map(getPopularity); //     .    ,    //   ,  reduce     ,  0. const scoresTotal = popularityScores.reduce(addScores, 0); //       . const averagePopularity = scoresTotal / popularityScores.length; console.log("Average popularity:", averagePopularity); 

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".

 //   // --------------------------------------------------------------------------------- function isFound(item) {    return item.found; }; function getPopularity(item) {    return item.popularity; } //    ,  return,   . function addScores({totalPopularity, itemCount}, popularity) {    return {        totalPopularity: totalPopularity + popularity,        itemCount:    itemCount + 1,    }; } //  // --------------------------------------------------------------------------------- const initialInfo  = {totalPopularity: 0, itemCount: 0}; const popularityInfo = victorianSlang.filter(isFound)    .map(getPopularity)    .reduce(addScores, initialInfo); //       . const {totalPopularity, itemCount} = popularityInfo; const averagePopularity = totalPopularity / itemCount; console.log("Average popularity:", averagePopularity); 

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.

 //   // ---------------------------------------------------------------------------- const filter = p => a => a.filter(p); const map   = f => a => a.map(f); const prop  = k => x => x[k]; const reduce = r => i => a => a.reduce(r, i); const compose = (...fns) => (arg) => fns.reduceRight((arg, fn) => fn(arg), arg); //  -   "blackbird combinator". //     : https://jrsinclair.com/articles/2019/compose-js-functions-multiple-parameters/ const B1 = f => g => h => x => f(g(x))(h(x)); //  // ---------------------------------------------------------------------------- //   sum,    . const sum = reduce((a, i) => a + i)(0); //     . const length = a => a.length; //       . const div = a => b => a / b; //   compose()        . //    compose()     . const calcPopularity = compose(    B1(div)(sum)(length),    map(prop('popularity')),    filter(prop('found')), ); const averagePopularity = calcPopularity(victorianSlang); console.log("Average popularity:", averagePopularity); 

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:

 //   ,  , : const avg1 = B1(div)(sum)(length); const avg2 = arr => div(sum(arr))(length(arr)); const avg3 = arr => ( sum(arr) / length(arr) ); const avg4 = arr => arr.reduce((a, x) => a + x, 0) / arr.length; 

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:

 //      // ---------------------------------------------------------------------------- function averageScores({avg, n}, slangTermInfo) {    if (!slangTermInfo.found) {        return {avg, n};       return {        avg: (slangTermInfo.popularity + n * avg) / (n + 1),        n:  n + 1,    }; } //  // ---------------------------------------------------------------------------- //       . const initialVals    = {avg: 0, n: 0}; const averagePopularity = victorianSlang.reduce(averageScores, initialVals).avg; console.log("Average popularity:", averagePopularity); 

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.

. . , . . , , .

?


? , . , - . , , , . , . , . , , .

, - , . , ? . . — .


:

  1. .reduce() .
  2. .filter() .map() , — .reduce() .
  3. , .
  4. .
  5. .

, -, ? — . - — , :

  1. , . — .
  2. , , — .
  3. , , — , .

! JavaScript-?

Source: https://habr.com/ru/post/458030/


All Articles