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

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


Array iteration methods are similar to “Starting drugs” (these are, of course, not drugs; and I do not say 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 seems that the .reduce () method causes some difficulties for many. It’s a little harder to understand.



I already wrote about why I think . reduce () creates many problems. This is partly due to the fact that many tutorials demonstrate using .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 a 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. Array length.

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

  function average (nums) {
 return nums.reduce ((a, b) = & gt; (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 situation, the calculation of the average value 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 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 estimate of the popularity of the expressions. Here’s what the data might look like (it’s 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 an 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 little.

  let popularity popularity = 0;
 let itemsFound = 0;
 const len ​​= victorianSlang.length;
 let item = null;
 for (let i = 0; i & lt; 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. Strictly speaking, the following happens here:

  1. We initialize the popularitySum and itemsFound variables. The first variable, popularitySum , stores the overall evaluation of the 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 an array.
  3. In the for loop, the i counter is incremented until its value reaches the index value of the last element of the array.
  4. Inside the loop, we take the array element that we want to explore. The element is accessed using the victorianSlang [i] construction.
  5. Then we find out if the given expression is 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. Finally, we find the average by dividing 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 the truth, “clean up” this decision.

2. Simple solution number 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's 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. Retrieve expressions popularity score objects. To solve this subtask, the .map () method is suitable.
  3. Calculate the amount of estimates. Here we can resort to the help of our old friend .reduce () .
  4. And finally, find the average value of the ratings.

Here’s how it looks in code:

 //Auxiliary functions//------------------------------------------------  ----------------------------
 function isFound (item) {
 return item.found;
 };

 function getPopularity (item) {
 return item.popularity;
 }

 function addScores (runningTotal, popularity) {
 return runningTotal + popularity;
 }
//Calculations//------------------------------------------------  ----------------------------
//Filter the expressions that were not found in the books.
 const foundSlangTerms = victorianSlang.filter (isFound);
//Extract the popularity scores by getting an array of numbers.
 const popularityScores = foundSlangTerms.map (getPopularity);
//Find the sum of all popularity ratings.  Note that the second parameter//indicates that reduce needs to use a battery initial value of 0.
 const scoresTotal = popularity scores.reduce (addScores, 0);
//Calculate and display the average value in the console.
 const averagePopularity = scoresTotal/popularityScores.length;
 console.log ("Average popularity:", averagePopularity);  

Look at the addScore function, and the line where .reduce () is called. Please note that addScore takes two parameters. The first, runningTotal , is known as a battery. It stores the sum of the values. Its value changes every time we iterate over the array and execute the return statement. The second parameter, popularity , is a separate element of the array that we process. At the very beginning of the array iteration, the return operator in addScore has never been executed. This means that the value of runningTotal has not yet been set automatically. Therefore, by calling .reduce () , we pass this value to the method that we need to write 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 .filter () , .map () and .reduce () arrays. 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: use 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 “flowing interface” design pattern in the program ( fluent interface ). 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.Please note 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 looking at the elements of the array, that is, using a “double battery”.

 //Auxiliary functions//------------------------------------------------  ---------------------------------
 function isFound (item) {
 return item.found;
 };

 function getPopularity (item) {
 return item.popularity;
 }
//To represent the multiple values ​​returned by return, we use an object.
 function addScores ({totalPopularity, itemCount}, popularity) {
 return {
 totalPopularity: totalPopularity + popularity,
 itemCount: itemCount + 1,
 };
 }
//Calculations//------------------------------------------------  ---------------------------------

 const initialInfo = {totalPopularity: 0, itemCount: 0};
 const popularityInfo = victorianSlang.filter (isFound)
 .map (getPopularity)
 .reduce (addScores, initialInfo);
//Calculate and display the average value in the console.
 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. Each time we walk through an array using addScrores , we update the overall popularity rating value and the number of items. 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 turned out to be 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 easy.

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 functional programming material on JavaScript, and, in particular, on the third part of the series, in which he enters.

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 options?

You can create a function to calculate the average using the compose () function alone, without using variables. We call it “programming without using fine notation” or “implicit programming”. In order to write such programs you 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 you to get better stack tracing results in case of errors.

 //Auxiliary functions//------------------------------------------------  ----------------------------
 const filter = p = & gt;  a = & gt;  a.filter (p);
 const map = f = & gt;  a = & gt;  a.map (f);
 const prop = k = & gt;  x = & gt;  x [k];
 const reduce = r = & gt;  i = & gt;  a = & gt;  a.reduce (r, i);
 const compose = (... fns) = & gt;  (arg) = & gt;  fns.reduceRight ((arg, fn) = & gt; fn (arg), arg);
//This is the so-called "blackbird combinator".//You can read about it here: https://jrsinclair.com/articles/2019/compose-js-functions-multiple-parameters/const B1 = f = & gt;  g = & gt;  h = & gt;  x = & gt;  f (g (x)) (h (x));
//Calculations//------------------------------------------------  ----------------------------
//Create a sum function that adds the elements of the array.
 const sum = reduce ((a, i) = & gt; a + i) (0);
//Function to get the length of the array.
 const length = a = & gt;  a.length;
//Function to divide one number by another.
 const div = a = & gt;  b = & gt;  a/b;
//We use compose () to build our function from small utility functions.//When working with compose (), the code should be read from bottom to top.
 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 is in the compose () function. If you read its contents from the bottom up, it turns out that the calculations begin with filtering the array on the property of its found elements. Then we retrieve the property of the popularity elements using 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:

 //All lines of code presented below are equivalent:
 const avg1 = B1 (div) (sum) (length);
 const avg2 = arr = & gt;  div (sum (arr)) (length (arr));
 const avg3 = arr = & gt;  (sum (arr)/length (arr));
 const avg4 = arr = & gt;  arr.reduce ((a, x) = & gt; 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 here.

5. Solving the problem in one run 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. 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 will suit, but in another record:


This formula is the same as this:


And the same thing 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 carried out in the reduction function:

 //Function for calculating the average value//------------------------------------------------  ----------------------------

 function averageScores ({avg, n}, slangTermInfo) {
 if (! slangTermInfo.found) {
 return {avg, n};

 return {
 avg: (slangTermInfo.popularity + n * avg)/(n + 1),
 n: n + 1,
 };
 }
//Calculations//------------------------------------------------  ----------------------------
//Calculate and display the average value in the console.
 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 only an object with two values ​​in memory.

However, such a memory efficiency has a certain price. Now in one function we perform three actions. We filter the array in it, extract the number and recalculate the result. This complicates the function. As a result, looking at the code is no longer so easy to understand.

What to choose?


Which of the above five approaches to solving the problem can be called the best? In fact, it depends on many factors. Perhaps you need to handle a really long array. Or perhaps your code needs to run on a platform that does not have a lot of memory available. In such cases, it makes sense to use the solution of the problem where array processing is performed in one pass. But if system constraints do not play a role, then you can successfully use more expressive approaches to solving the problem. The programmer needs to analyze his own situation and decide what best suits his application, what is most appropriate to use in his circumstances.

Perhaps someone will now have the question of whether there is a way to combine the advantages of different approaches to solving such a problem. Is it possible to break the task into small parts, but to perform all the calculations in one pass through the array? You can do it. To do this, you need to apply the concept of transducers. This is a separate big topic.

Totals


We looked at five ways to calculate the average for the elements of an array:

  1. Without using .reduce () .
  2. Using the .filter () and .map () methods, as well as the .reduce () method as a mechanism for finding sums of numbers.
  3. With the use of a battery, which is an object and stores multiple values.
  4. Using implicit programming techniques.
  5. With the calculation of the cumulative average in a single pass through the array.

What, after all, should choose for practical use? In fact - it's up to you. But if you want to find some clue - I will share my opinion on how you can decide on the most appropriate way to solve the problem:

  1. Start by using the approach that you understand best. If it allows you to reach your goal, stop at it.
  2. If there is a certain approach that you don’t understand, but want to explore, solve the problem with it.
  3. And finally, if you are faced with a memory shortage problem, try the option where the array costs once.

Dear readers! How do you most often handle arrays in JavaScript projects?

Source text: [Translation] Functional JavaScript: five ways to find the arithmetic average of the elements of the array and the method .reduce ()