JavaScript funcional: cinco maneiras de encontrar a média aritmética dos elementos da matriz e o método .reduce ()

Os métodos de iteração de matriz são semelhantes a “iniciar drogas” (é claro, eles não são drogas; e eu não estou dizendo que drogas são boas; são apenas uma figura de linguagem). Por causa deles, muitos "se sentam" em programação funcional. O fato é que eles são incrivelmente convenientes. Além disso, a maioria desses métodos é muito fácil de entender. Métodos como .map() e .filter() aceitam apenas um argumento de retorno de chamada e permitem resolver problemas simples. Mas há um sentimento de que o método .reduce() causa algumas dificuldades para muitos. Entender isso é um pouco mais difícil.



Eu já escrevi sobre por que acho que .reduce() cria muitos problemas. Isso se deve em parte ao fato de muitos manuais demonstrarem o uso de .reduce() apenas ao manusear números. Portanto, escrevi sobre quantas tarefas que não implicam operações aritméticas podem ser resolvidas usando .reduce() . Mas e se você absolutamente precisar trabalhar com números?

Um uso típico de .reduce() parece com um cálculo da média aritmética dos elementos de uma matriz. À primeira vista, parece que não há nada de especial nessa tarefa. Mas ela não é tão simples. O fato é que, antes de calcular a média, você precisa encontrar os seguintes indicadores:

  1. A quantidade total de valores do elemento da matriz.
  2. O comprimento da matriz.

Descobrir tudo isso é bem simples. E o cálculo dos valores médios para matrizes numéricas também não é uma operação difícil. Aqui está um exemplo elementar:

 function average(nums) {    return nums.reduce((a, b) => (a + b)) / nums.length; } 

Como você pode ver, não há incompreensões especiais aqui. Mas a tarefa se torna mais difícil se você precisar trabalhar com estruturas de dados mais complexas. E se tivermos uma matriz de objetos? E se alguns objetos dessa matriz precisarem ser filtrados? O que fazer se você precisar extrair certos valores numéricos dos objetos? Nessa situação, calcular o valor médio dos elementos da matriz já é uma tarefa um pouco mais complicada.

Para lidar com isso, resolveremos o problema de treinamento (é baseado nesta tarefa com o FreeCodeCamp). Vamos resolvê-lo de cinco maneiras diferentes. Cada um deles tem suas próprias vantagens e desvantagens. Uma análise dessas cinco abordagens para resolver esse problema mostra como o JavaScript pode ser flexível. E espero que a análise das soluções dê a você um pensamento sobre como usar .reduce() em projetos reais.

Visão geral da tarefa


Suponha que tenhamos uma série de objetos que descrevem expressões de gírias vitorianas. Você precisa filtrar as expressões que não são encontradas no Google Livros (a propriedade found dos objetos correspondentes é false ) e encontrar uma classificação média para a popularidade das expressões. Veja como esses dados podem ser (extraídos daqui ):

 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 maneiras de encontrar o valor médio da avaliação da popularidade das expressões dessa matriz.

1. Resolvendo um problema sem usar .reduce () (loop imperativo)


Em nossa primeira abordagem para resolver o problema, o método .reduce() não será usado. Se você nunca encontrou métodos para iterar matrizes antes, espero que a análise deste exemplo esclareça um pouco a situação.

 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); 

Se você estiver familiarizado com JavaScript, entenderá facilmente este exemplo. De fato, acontece o seguinte aqui:

  1. Inicializamos as variáveis popularitySum e itemsFound . A primeira variável, popularitySum , armazena a classificação geral da popularidade das expressões. E a segunda variável, itemsFound , (isso é uma surpresa) armazena o número de expressões encontradas.
  2. Em seguida, inicializamos a constante len e o item variável, que são úteis para nós ao atravessar a matriz.
  3. Em um loop for , o contador i incrementado até que seu valor atinja o valor de índice do último elemento da matriz.
  4. Dentro do loop, pegamos o elemento da matriz que queremos explorar. Acessamos o elemento usando a construção victorianSlang[i] .
  5. Então descobrimos se essa expressão é encontrada na coleção de livros.
  6. Se uma expressão ocorrer nos livros, pegamos o valor da sua classificação de popularidade e adicionamos ao valor da variável popularitySum .
  7. Ao mesmo tempo, também aumentamos o contador das expressões encontradas - itemsFound .
  8. E, finalmente, encontramos a média dividindo a popularitySum por itemsFound .

Então, lidamos com a tarefa. Talvez nossa decisão não tenha sido particularmente bonita, mas faz seu trabalho. O uso de métodos para percorrer matrizes o tornará um pouco mais limpo. Vamos ver se conseguimos, e a verdade é, "limpar" essa decisão.

2. Solução simples nº 1: .filter (), .map () e localize a quantidade usando .reduce ()


Vamos, antes da primeira tentativa de usar os métodos de matrizes para resolver o problema, dividimos em partes pequenas. Ou seja, aqui está o que precisamos fazer:

  1. Selecione objetos que representam expressões que estão na coleção do Google Livros. Aqui você pode usar o método .filter() .
  2. Extraia dos objetos a avaliação da popularidade das expressões. Para resolver essa subtarefa, o método .map() é adequado.
  3. Calcule a soma das classificações. Aqui podemos recorrer à ajuda de nosso velho amigo .reduce() .
  4. E, finalmente, encontre o valor médio das estimativas.

Aqui está o que parece no 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); 

addScore olhada na função addScore e na linha onde .reduce() chamado. Observe que o addScore aceita dois parâmetros. O primeiro, runningTotal , é conhecido como bateria. Ele armazena a soma dos valores. Seu valor muda sempre que iteramos sobre a matriz e executamos a return . O segundo parâmetro, popularity , é um elemento separado da matriz que estamos processando. No início da addScore sobre a matriz, a return addScore no addScore nunca foi executada. Isso significa que runningTotal não foi definido automaticamente. Portanto, chamando .reduce() , passamos para esse método o valor que precisa ser gravado em runningTotal no início. Este é o segundo parâmetro passado para .reduce() .

Portanto, aplicamos os métodos de iteração de matrizes para resolver o problema. A nova versão da solução acabou sendo muito mais limpa que a anterior. Em outras palavras, a decisão acabou sendo mais declarativa. Não informamos ao JavaScript exatamente como executar o loop; não seguimos os índices dos elementos das matrizes. Em vez disso, declaramos funções auxiliares simples de tamanho pequeno e as combinamos. Todo o trabalho duro é feito para nós pelos métodos de matriz .filter() , .map() e .reduce() . Essa abordagem para resolver esses problemas é mais expressiva. Esses métodos de matriz são muito mais completos do que o loop pode fazer, eles nos informam sobre a intenção estabelecida no código.

3. Solução fácil # 2: Usando várias baterias


Na versão anterior da solução, criamos um monte de variáveis ​​intermediárias. Por exemplo, foundSlangTerms e popularityScores . No nosso caso, essa solução é bastante aceitável. Mas e se nos definirmos uma meta mais complexa em relação ao design de código? Seria bom se pudéssemos usar o padrão de design de interface fluente no programa. Com essa abordagem, poderíamos encadear as chamadas de todas as funções e podermos fazer sem variáveis ​​intermediárias. No entanto, um problema nos espera aqui. Observe que precisamos obter o valor de popularityScores.length . Se vamos encadear tudo, precisamos de outra maneira de encontrar o número de elementos na matriz. O número de elementos na matriz desempenha o papel de um divisor no cálculo do valor médio. Vamos ver se podemos mudar a abordagem para resolver o problema para que tudo possa ser feito combinando chamadas de método em uma cadeia. Faremos isso rastreando dois valores ao iterar sobre os elementos da matriz, ou seja, usando a "bateria dupla".

 //   // --------------------------------------------------------------------------------- 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); 

Aqui, para trabalhar com dois valores, usamos o objeto na função redutora. Cada passagem pela matriz realizada usando addScrores , atualizamos o valor total da classificação de popularidade e o número de elementos. É importante observar que esses dois valores são representados como um único objeto. Com essa abordagem, podemos "enganar" o sistema e armazenar duas entidades dentro do mesmo valor de retorno.

A função addScrores sendo um pouco mais complicada do que a função com o mesmo nome no exemplo anterior. Agora, porém, podemos usar uma única cadeia de chamadas de método para executar todas as operações com a matriz. Como resultado do processamento da matriz, obtemos o objeto popularityInfo , que armazena tudo o que você precisa para encontrar a média. Isso torna a cadeia de chamadas organizada e simples.

Se você deseja melhorar esse código, pode experimentar. Por exemplo - você pode refazê-lo para se livrar de muitas variáveis ​​intermediárias. Você pode até tentar colocar esse código em uma linha.

4. Composição de funções sem usar notação de ponto


Se você não conhece a programação funcional ou se parece que a programação funcional é muito complicada, pode pular esta seção. Analisá-lo o beneficiará se você já estiver familiarizado com curry() e compose() . Se você quiser se aprofundar neste tópico, dê uma olhada neste material sobre programação funcional em JavaScript e, em particular, na terceira parte da série em que está incluído.

Somos programadores que adotam uma abordagem funcional. Isso significa que nos esforçamos para criar funções complexas a partir de outras funções - pequenas e simples. Até o momento, ao considerar várias opções para resolver o problema, reduzimos o número de variáveis ​​intermediárias. Como resultado, o código da solução tornou-se mais simples e fácil. Mas e se essa idéia for levada ao extremo? E se você tentar se livrar de todas as variáveis ​​intermediárias? E até tentar fugir de alguns parâmetros?

Você pode criar uma função para calcular a média usando a função compose() sozinha, sem usar variáveis. Chamamos isso de “programação sem o uso de notação refinada” ou “programação implícita”. Para escrever esses programas, você precisará de muitas funções auxiliares.

Às vezes, esse código choca as pessoas. Isso se deve ao fato de que tal abordagem é muito diferente da geralmente aceita. Mas descobri que escrever código no estilo de programação implícita é uma das maneiras mais rápidas de entender a essência da programação funcional. Portanto, posso aconselhá-lo a tentar esta técnica em algum projeto pessoal. Mas quero dizer que talvez você não deva escrever no estilo de programação implícita o código que outras pessoas precisam ler.

Então, voltando à nossa tarefa de construir um sistema para calcular médias. Por uma questão de economia de espaço, passaremos aqui para o uso das funções de seta. Geralmente, como regra, é melhor usar funções nomeadas. Aqui está um bom artigo sobre este tópico. Isso permite que você obtenha melhores resultados de rastreamento de pilha em caso de erros.

 //   // ---------------------------------------------------------------------------- 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); 

Se todo esse código lhe parecer absurdo - não se preocupe. Incluí aqui como um exercício intelectual, e não para incomodá-lo.

Nesse caso, o trabalho principal está na função compose() . Se você ler o conteúdo de baixo para cima, os cálculos começam filtrando a matriz pela propriedade de seus elementos found . Em seguida, recuperamos a propriedade do elemento popularity usando map() . Depois disso, usamos o chamado “ combinador de melro ”. Essa entidade é representada como uma função B1 , que é usada para executar duas passagens de cálculos em um conjunto de dados de entrada. Para entender melhor isso, dê uma olhada nestes exemplos:

 //   ,  , : 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; 

Novamente, se você não entender nada de novo - não se preocupe. Esta é apenas uma demonstração de que o JavaScript pode ser escrito de maneiras muito diferentes. Dessas características, essa é a beleza desse idioma.

5. Resolvendo o problema de uma só vez com o cálculo do valor médio acumulado


Todas as construções de software acima fazem um bom trabalho para resolver nosso problema (incluindo o ciclo imperativo). Aqueles que usam o método .reduce() têm algo em comum. Eles são baseados em dividir o problema em pequenos fragmentos. Esses fragmentos são então montados de várias maneiras. Ao analisar essas soluções, você pode perceber que nelas contornamos o array três vezes. Há um sentimento de que é ineficaz. Seria bom se houvesse uma maneira de processar a matriz e retornar o resultado em uma única passagem. Este método existe, mas sua aplicação exigirá o recurso à matemática.

Para calcular o valor médio dos elementos da matriz em uma passagem, precisamos de um novo método. Você precisa encontrar uma maneira de calcular a média usando a média calculada anteriormente e o novo valor. Procuramos esse método usando álgebra.

O valor médio de n números pode ser encontrado usando esta fórmula:


Para descobrir os números n + 1 médios, a mesma fórmula serve, mas em uma entrada diferente:


Esta fórmula é a mesma que esta:


E a mesma coisa que isso:


Se você converter um pouco, obterá o seguinte:


Se você não vê o ponto disso tudo, tudo bem. O resultado de todas essas transformações é que, com a ajuda da última fórmula, podemos calcular o valor médio durante uma única travessia da matriz. Para isso, é necessário conhecer o valor do elemento atual, o valor médio calculado na etapa anterior e o número de elementos. Além disso, a maioria dos cálculos pode ser realizada na função redutora:

 //      // ---------------------------------------------------------------------------- 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); 

Graças a essa abordagem, o valor necessário pode ser encontrado ignorando a matriz apenas uma vez. Outras abordagens usam uma passagem para filtrar a matriz, outra para extrair os dados necessários e outra para encontrar a soma dos valores dos elementos. Aqui, tudo se encaixa em uma passagem pela matriz.

Observe que isso não necessariamente torna os cálculos mais eficientes. Com essa abordagem, mais cálculos precisam ser feitos. Quando cada novo valor chega, realizamos as operações de multiplicação e divisão, fazendo isso para manter o valor médio atual no estado atual. Em outras soluções para esse problema, dividimos um número em outro apenas uma vez - no final do programa. Mas essa abordagem é muito mais eficiente em termos de uso de memória. Matrizes intermediárias não são usadas aqui, como resultado, temos que armazenar na memória apenas um objeto com dois valores.

. . , . . , , .

?


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

, - , . , ? . . — .


:

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

, -, ? — . - — , :

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

! JavaScript-?

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


All Articles