📜 ⬆️ ⬇️

Functional JavaScript: What are higher order functions and why are they needed?


“Higher Order Functions” is one of those phrases that are often scattered. But rarely can anyone stop and explain what it is. Perhaps you already know what is called higher order functions. But how do we use them in real projects? When and why are they useful? Can we manipulate the DOM with their help? Or do people who use these functions just brag? Perhaps they are meaninglessly complicating the code?

I used to think that higher order functions are useful. Now I consider them the most important feature of JavaScript as a language. But before we discuss this, let's first understand what the higher order functions are. And we start with functions as variables.

Functions as first class objects


In JavaScript, there are at least three ways (there are more of them) to write a new function. First, you can write a function declaration :

// Take a DOM element and wrap it in a list item element. function itemise(el) { const li = document.createElement('li'); li.appendChild(el); return li; } 

I hope you understand everything. You also probably know that you can write a function expression :
')
 const itemise = function(el) { const li = document.createElement('li'); li.appendChild(el); return li; } 

And finally, there is another way to write the same function - as an arrow function :

 const itemise = (el) => { const li = document.createElement('li'); li.appendChild(el); return li; } 

In this case, all three methods are equivalent. Although this is not always the case, in practice, each method has small differences related to what happens to the magic of a particular keyword and the labels in the stack paths.

But note that the last two examples assign a function to a variable. It looks like a trifle. Why not assign a function to a variable? But it is very important. Functions in JavaScript belong to the " first class ". Therefore, we can:


This is wonderful, but what does all this have to do with higher order functions? Pay attention to the last two points. Soon we will return to them, but for now let's look at a few examples.

We saw the assignment of functions to variables. What about passing them as parameters? Let's write a function that can be used with DOM elements. If we execute document.querySelectorAll() , then the answer is not an array, but NodeList . NodeList no .map() method, like arrays, so let's write this:

 // Apply a given function to every item in a NodeList and return an array. function elListMap(transform, list) { // list might be a NodeList, which doesn't have .map(), so we convert // it to an array. return [...list].map(transform); } // Grab all the spans on the page with the class 'for-listing'. const mySpans = document.querySelectorAll('span.for-listing'); // Wrap each one inside an <li> element. We re-use the // itemise() function from earlier. const wrappedList = elListMap(itemise, mySpans); 

Here we pass the itemise function as an argument to the elListMap function. But we can use elListMap not only to create lists. For example, with its help you can add a class to a set of elements:

 function addSpinnerClass(el) { el.classList.add('spinner'); return el; } // Find all the buttons with class 'loader' const loadButtons = document.querySelectorAll('button.loader'); // Add the spinner class to all the buttons we found. elListMap(addSpinnerClass, loadButtons); 

elLlistMap takes another function as a parameter and converts it. That is, we can use elListMap to solve different problems.

We looked at an example of passing functions as parameters. Now let's talk about returning a function from a function. What does this look like?

We first write the usual old function. We need to take a list of li elements and wrap in ul . Easy:

 function wrapWithUl(children) { const ul = document.createElement('ul'); return [...children].reduce((listEl, child) => { listEl.appendChild(child); return listEl; }, ul); } 

And if then we have a bunch of paragraph elements that we want to wrap in a div ? No problem, we will write another function for this:

 function wrapWithDiv(children) { const div = document.createElement('div'); return [...children].reduce((divEl, child) => { divEl.appendChild(child); return divEl; }, div); } 

Works great. However, these two functions are very similar, the difference is only in the parent element that we created.

Now we could write a function that takes two parameters: the type of the parent element and a list of child elements. But there is another option. We can create a function that returns a function. For example:

 function createListWrapperFunction(elementType) { // Straight away, we return a function. return function wrap(children) { // Inside our wrap function, we can 'see' the elementType parameter. const parent = document.createElement(elementType); return [...children].reduce((parentEl, child) => { parentEl.appendChild(child); return parentEl; }, parent); } } 

At first, it may look a little difficult, so let's divide the code. We created a function that just returns another function. But this return function remembers the elementType parameter. And then, when we call the returned function, it already knows which element to create. Therefore, you can create wrapWithUl and wrapWithDiv :

 const wrapWithUl = createListWrapperFunction('ul'); // Our wrapWithUl() function now 'remembers' that it creates a ul element. const wrapWithDiv = createListWreapperFunction('div'); // Our wrapWithDiv() function now 'remembers' that it creates a div element. 

This trick, when the returned function "remembers" about something, is called a closure . You can read more about them here . Closures are incredibly convenient, but for the time being we will not think about them.

So, we have disassembled:


In general, first-class functions are a nice thing. But what have the higher order functions ? Let's look at the definition.

What is a higher order function?


Definition : This is a function that takes a function as an argument or returns a function as a result.

Familiar? In JavaScript, these are first-class functions. That is, “higher order functions” have exactly the same advantages. In other words, this is just a fancy name for a simple idea.

Examples of higher order functions


If you start looking, you start to notice higher-order functions everywhere. The most common are functions that take other functions as parameters.

Functions that take other functions as parameters


When you pass a callback, you use a higher order function. In front-end development, they are found everywhere. One of the most common is the .addEventListener() method. We use it when we want to perform actions in response to some events. For example, I want to make a button that issues a warning:

 function showAlert() { alert('Fallacies do not cease to be fallacies because they become fashions'); } document.body.innerHTML += `<button type="button" class="js-alertbtn"> Show alert </button>`; const btn = document.querySelector('.js-alertbtn'); btn.addEventListener('click', showAlert); 

Here we created a function that shows a warning, added a button to the page, and passed the showAlert() function as an argument to btn.addEventListener() .

We also encounter higher-order functions when using array iteration methods : for example, .map() , .filter() and .reduce() . As in the elListMap() function:

 function elListMap(transform, list) { return [...list].map(transform); } 

Higher order functions also help to work with delays and timing. The setTimeout() and setInterval() functions help control when functions are executed. For example, if you need to remove the highlight class after 30 seconds, you can do it like this:

 function removeHighlights() { const highlightedElements = document.querySelectorAll('.highlighted'); elListMap(el => el.classList.remove('highlighted'), highlightedElements); } setTimeout(removeHighlights, 30000); 

Again, we created a function and passed it to another function as an argument.

As you can see, in JavaScript, functions that take on other functions are often found. And you probably already use them.

Functions Returning Functions


Functions of this type are not as common as the previous ones. But they are also useful. One of the best examples is the maybe () function. I adapted a variant from the Allongé JavaScript book :

 function maybe(fn) return function _maybe(...args) { // Note that the == is deliberate. if ((args.length === 0) || args.some(a => (a == null)) { return undefined; } return fn.apply(this, args); } } 

Instead of understanding the code, let's first look at how it can be applied. Let's look again at the elListMap() function:

 // Apply a given function to every item in a NodeList and return an array. function elListMap(transform, list) { // list might be a NodeList, which doesn't have .map(), so we convert // it to an array. return [...list].map(transform); } 

What happens if you accidentally pass in elListMap() null or undefined value? We get a TypeError and drop the current operation, whatever it is. You can avoid this by using the maybe() function:

 const safeElListMap = maybe(elListMap); safeElListMap(x => x, null); // ← undefined 

Instead of dropping, the function will return undefined . And if we passed it to another function protected by maybe() , then in response we would get undefined again. With maybe() you can protect any number of functions, it is much easier to write a billion if .

Functions that return functions are also often found in the React world. For example, connect() .

So, what is next?


We have seen several examples of using higher order functions. So, what is next? What can they give us something that we cannot get without them?

To answer this question, let's consider another example - the built-in array .sort() method. Yes, he has flaws. It changes the array instead of returning a new one. But let's forget about it for now. The .sort() method is a higher order function; it takes another function as one of the parameters.

How it works? If we want to sort an array of numbers, we first need to create a comparison function:

 function compareNumbers(a, b) { if (a === b) return 0; if (a > b) return 1; /* else */ return -1; } 

Now sort the array:

 let nums = [7, 3, 1, 5, 8, 9, 6, 4, 2]; nums.sort(compareNumbers); console.log(nums); // 〕[1, 2, 3, 4, 5, 6, 7, 8, 9] 

You can also sort lists of numbers. But what is the use of it? How often do we have a list of numbers to sort? Infrequently. Usually I need to sort an array of objects:

 let typeaheadMatches = [ { keyword: 'bogey', weight: 0.25, matchedChars: ['bog'], }, { keyword: 'bog', weight: 0.5, matchedChars: ['bog'], }, { keyword: 'boggle', weight: 0.3, matchedChars: ['bog'], }, { keyword: 'bogey', weight: 0.25, matchedChars: ['bog'], }, { keyword: 'toboggan', weight: 0.15, matchedChars: ['bog'], }, { keyword: 'bag', weight: 0.1, matchedChars: ['b', 'g'], } ]; 

Suppose I want to sort this array by the weight of each record. I could write from scratch a new sorting function. But why, if you can create a new comparison function:

 function compareTypeaheadResult(word1, word2) { return -1 * compareNumbers(word1.weight, word2.weight); } typeaheadMatches.sort(compareTypeaheadResult); console.log(typeaheadMatches); // 〕[{keyword: "bog", weight: 0.5, matchedChars: ["bog"]}, … ] 

You can write a comparison function for any kind of arrays. The .sort() method helps us: “If you give me a comparison function, I will sort out any array. Do not worry about its contents. If you give a sort function, I will sort it out. ” Therefore, we do not need to write the sorting algorithm ourselves; we will focus on a much simpler problem of comparing two elements.

Now imagine that we do not use higher-order functions. We cannot pass a function to the .sort() method. We will have to write a new sort function every time we need to sort an array of a different kind. Or you have to reinvent the same with function pointers or objects. In any case, it will be very awkward.

However, we have higher-order functions that allow us to separate the sort function from the compare function. Suppose a smart browser developer has updated .sort() to use a faster algorithm. Then your code will benefit, regardless of what is inside the sorted arrays. And this scheme is true for a whole set of higher-order array functions .

This leads us to this idea. The .sort() method abstracts the sorting task from the contents of an array. This is called separation of concerns. Higher-order functions allow you to create abstractions that would be very cumbersome or even impossible without them. And the creation of abstractions accounts for 80% of the work of software engineers.

When we refactor the code to remove repetitions, we create abstractions. We see the pattern and replace it with an abstract representation. As a result, the code becomes more meaningful and easier to understand. At least that is the goal.

Higher-order functions are a powerful tool for creating abstractions. And with abstractions there is a whole branch of mathematics, the theory of categories. More precisely, the theory of categories is devoted to the search for abstractions of abstractions. In other words, we are talking about the search patterns patterns. And over the past 70 years, smart programmers have borrowed a lot of ideas from there, which have become properties of languages ​​and libraries. If we learn these patterns of patterns, then sometimes we can replace large pieces of code. Or simplify complex problems to elegant combinations of simple building blocks. These blocks are higher order functions. Therefore, they are so important, they give us a powerful tool to deal with the complexity of our code.

Additional materials about higher order functions:


You are probably already using higher order functions. It's so easy in javascript that we don't even think about it. But it's better to know what people are saying when they say this phrase. This is not difficult. But behind the simple idea is a great power.

If you are experienced in functional programming, you might have noticed that I used non-pure functions and some ... verbose function names. This is not because I have not heard about impure functions or general principles of functional programming. And I do not write such code in production. I tried to pick up practical examples that will be understandable to beginners. Sometimes I had to make compromises. If interested, then I already wrote about functional cleanliness and the general principles of functional programming .

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


All Articles