📜 ⬆️ ⬇️

Functional JavaScript: five ways to find the arithmetic average of array elements and the .reduce () method

Array iteration methods are similar to “starting drugs” (these are, of course, not drugs; and I’m not saying that drugs are good; they are just a figure of speech). Because of them, many "sit down" on functional programming. The thing is, they are incredibly comfortable. In addition, most of these methods are very simple to understand. Methods like .map() and .filter() take just one callback argument and allow you to solve simple problems. But it .reduce() that the .reduce() method causes certain difficulties for many. To understand it is a little more difficult.



I already wrote about why I think that .reduce() creates a lot of problems. In part, this is due to the fact that many manuals demonstrate the use of .reduce() only when processing numbers. That's why I wrote about how many tasks that do not imply the execution of arithmetic operations can be solved using .reduce() . But what if you absolutely need to work with numbers?

A typical case of using .reduce() looks like the calculation of the arithmetic average of the elements of an array. At first glance it seems that there is nothing special in this task. But it is not so simple. The fact is that before you calculate the average, you need to find the following indicators:
')
  1. The total sum of the values ​​of the elements of the array.
  2. The length of the array.

It’s pretty easy to figure out. And the calculation of average values ​​for numeric arrays is also not a complex operation. Here is an elementary example:

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

As you can see, there are no special incomprehensibilities. But the task becomes harder if you have to work with more complex data structures. What if we have an array of objects? What if some objects from this array need to be filtered? What if objects need to extract certain numerical values? In this scenario, the calculation of the average for the elements of the array is already a bit more complicated task.

In order to deal with this, we will solve the learning problem (it is based on this task with FreeCodeCamp). We will solve it in five different ways. Each of them has its own advantages and disadvantages. An analysis of these five approaches to solving this problem will show how flexible JavaScript can be. And I hope that decision analysis will give you some food for thought about how to use .reduce() in real projects.

Task Overview


Suppose that we have an array of objects describing slang expressions of the Victorian era. You need to filter out those expressions that are not found in Google Books (the found property of the corresponding objects is false ) and find the average rating of the popularity of expressions. Here is what similar data might look like (it is taken from here ):

 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,    }, ]; 

Consider 5 ways to find the average estimate of the popularity of expressions from this array.

1. Solution of the problem without using .reduce () (imperative cycle)


In our first approach to solving the problem, the .reduce() method will not be used. If you have not come across methods for iterating arrays before, then I hope the analysis of this example will clarify the situation for you a bit.

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

If you are familiar with JavaScript, you will easily understand this example. In fact, the following happens here:

  1. We initialize the variables itemsFound and itemsFound . The first variable, popularitySum , stores the overall popularity of expressions. And the second variable, itemsFound , (that's a surprise) stores the number of expressions found.
  2. Then we initialize the len constant and the item variable, which will be useful to us when traversing the array.
  3. In the for loop, counter i incremented until its value reaches the index value of the last element of the array.
  4. Inside the loop, we take the element of the array that we want to explore. The element is accessed using the victorianSlang[i] construction.
  5. Then we find out if the given expression is found in the book collection.
  6. If the expression in the books is found - we take the value of its popularity rating and add to the value of the variable popularitySum .
  7. At the same time, we also increase the counter of found expressions - itemsFound .
  8. And finally, we find the average value, dividing the popularitySum by itemsFound .

So, we coped with the task. Perhaps the solution we got was not particularly beautiful, but it does its job. Using methods to iterate arrays will make it a little cleaner. Let's take a look at whether we can, and really, “clean” this solution.

2. Simple solution №1: .filter (), .map () and finding the amount using .reduce ()


Let's, before the first attempt to use the array methods to solve the problem, break it into small parts. Namely, here is what we need to do:

  1. Select objects representing expressions that are in the Google Books collection. Here you can use the .filter() method.
  2. Extract expressions from the object of evaluation. To solve this subtask, the .map() method is suitable.
  3. Calculate the amount of estimates. Here we can resort to using our old friend .reduce() .
  4. And finally, find the average value of the estimates.

Here is how it looks in code:

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

Look at the addScore function, and the line where .reduce() called. Notice that the addScore takes two parameters. The first, runningTotal , is known as a battery. It stores the sum of the values. Its value changes every time when we iterate over the array and execute the return . The second parameter, popularity , is a separate element of the array that we process. At the very beginning of the array addScore , the return in addScore has never been executed yet. This means that the runningTotal value runningTotal not yet been set automatically. Therefore, by calling .reduce() , we pass this value to the method that needs to be written to runningTotal at the very beginning. This is the second parameter passed to .reduce() .

So, we used to solve the problem the methods of array iteration. The new version of the solution turned out much cleaner than the previous one. In other words, the decision was more declarative. We do not tell JavaScript about exactly how to execute the loop, do not follow the indexes of the elements of the arrays. Instead, we declare simple auxiliary functions of small size and combine them. All the hard work is done for us by the array .filter() , .map() and .reduce() . This approach to solving such problems is more expressive. These array methods are much more complete than a cycle can do; they inform us of the intent contained in the code.

3. Simple solution number 2: the use of multiple batteries


In the previous version of the solution, we created a whole bunch of intermediate variables. For example - foundSlangTerms and popularityScores . In our case, this solution is quite acceptable. But what if we set a more complex goal for the device code? It would be nice if we could use the fluent interface design pattern in the program. With this approach, we could chain the calls of all functions and be able to do without intermediate variables. However, there is one problem waiting for us. Pay attention to the fact that we need to get the value of popularityScores.length . If we are going to combine everything into a chain, then we need some other way to find the number of elements in the array. The number of elements in the array plays the role of the divisor in calculating the average value. Let's see if we can change the approach to solving the problem so that everything can be done by combining the method calls into a chain. We will do this by tracking two values ​​when sorting the elements of an array, that is, using a “double battery”.

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

Here we, for work with two values, used an object in the reduction function. With each pass through the array, performed using addScrores , we update the total value of the popularity rating and the number of elements. It is important to note that these two values ​​are represented as a single object. With this approach, we can "deceive" the system and store two entities within the same return value.

The addScrores function was a bit more complex than the function with the same name of the previous example. But now it turns out that we can use a single chain of method calls to perform all operations with an array. As a result of array processing, a popularityInfo object is obtained, which stores everything needed to find the average. This makes the call chain neat and simple.

If you feel the desire to improve this code, then you can experiment with it. For example - you can remake it to get rid of the set of intermediate variables. This code can even try to put in one line.

4. Composition of functions without the use of dot notation


If you are new to functional programming, or if you think that functional programming is too difficult, you can skip this section. His parsing will benefit you if you are already familiar with curry() and compose() . If you want to delve into this topic - take a look at this material on functional programming in JavaScript, and, in particular, on the third part of the series, in which it is included.

We are programmers who adhere to the functional approach. This means that we strive to build complex functions from other functions — small and simple. So far, in the course of considering various options for solving the problem, we have reduced the number of intermediate variables. As a result, the solution code became simpler and simpler. But what if you bring this idea to the extreme? What if you try to get rid of all intermediate variables? And even try to get away from some of the parameters?

You can create a function to calculate the average using only the compose() function, without using variables. We call it “programming without using fine notation” or “implicit programming”. In order to write such programs will need a lot of auxiliary functions.

Sometimes this code shocks people. This is due to the fact that this approach is very different from the conventional one. But I found out that writing code in the style of implicit programming is one of the fastest ways to get into the essence of functional programming. Therefore, I can advise you to try this technique in any personal project. But I want to say that perhaps you should not write in the style of implicit programming the code that other people have to read.

So, back to our task of building a system for computing average values. For the sake of space, we will use the switch functions here. Usually, it is usually better to use named functions. Here is a good article on this topic. This allows us to obtain better stack trace results in the event of errors.

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

If all this code seems to you utter nonsense - do not worry about it. I included it here as an intellectual exercise, not to upset you.

In this case, the main work goes to the compose() function. If you read its contents from the bottom up, it turns out that the calculations begin with filtering the array by the property of its found elements. Then we retrieve the property of the popularity elements with map() . After that we use the so-called “ blackbird combinator ”. This entity is represented as a function B1 , which is used to perform two computation passes on one set of input data. To better understand this, take a look at these examples:

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

Again, if you again did not understand - do not worry. This is just a demonstration of the fact that you can write in JavaScript in very different ways. The beauty of this language is made up of such features.

5. Solution of the problem in one pass with the calculation of the cumulative average value


All the above program constructions cope well with the solution of our problem (including the imperative cycle). Those that use the .reduce() method have something in common. They are based on breaking the problem into small fragments. These fragments are then assembled in various ways. Analyzing these solutions, you may have noticed that in them we go around the array three times. There is a feeling that it is ineffective. It would be nice if there was a way to process the array and return the result in one pass. Such a method exists, but its application will require resort to mathematics.

In order to calculate the average value for the elements of the array in one pass, we need a new method. We need to find a way to calculate the average using the previously calculated average and new value. Let's look for this method using algebra.

The average value of n numbers can be found using the following formula:


In order to find out the average n + 1 numbers, the same formula is suitable, but in another record:


This formula is the same as this:


And the same as this:


If you convert this a little bit, you get the following:


If you do not see in all this sense - do not worry. The result of all these transformations is that with the help of the last formula we can calculate the average value in the process of a single bypass of the array. To do this, you need to know the value of the current element, the average value calculated at the previous step, and the number of elements. In addition, most of the calculations can be made to the reducer function:

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

By using this approach, the required value can be found by going around the array only once. Other approaches use one pass to filter the array, another one to extract the necessary data from it, and another one to find the sum of the values ​​of the elements. Here, everything fits in a single pass through the array.

Note that this does not necessarily make the calculations more efficient. With this approach, it is necessary to perform more calculations. When entering each new value, we perform multiplication and division operations, doing this to keep the current mean value up to date. In other versions of this task, we divide one number into another only once - at the end of the program. But this approach is much more efficient in terms of memory usage. Intermediate arrays are not used here, as a result we have to store in memory only an object with two values.

. . , . . , , .

?


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

, - , . , ? . . — .

Results


:

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

, -, ? — . - — , :

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

Dear readers! JavaScript-?

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


All Articles