📜 ⬆️ ⬇️

[Translation] Do not be afraid of functional programming

I present to your attention the translation of Jonathan Morgan’s article on functional programming that has recently slipped into the digest links on the example of JavaScript. The material is designed for beginners, but nevertheless it is quite interesting.

I would be grateful for constructive comments and suggestions for typos, translation and / or design. Enjoy reading!


Do not be afraid of functional programming.


Functional programming is a mustache hipster from programming paradigms. Being a product of the times of the academic development of computer science, functional programming not long ago experienced a revival due to its great use when used in distributed systems (and, probably, due to the fact that “pure” functional languages ​​like Haskell are not easy to understand, which also leaves its mark).
')
More rigorous functional programming languages ​​are usually used when performance and coherence are critical - for example, if a program should always do exactly what is expected of it and work in an environment where tasks can be divided between hundreds and thousands of networked ones computers. Clojure, for example, is at the core of Akamai, the huge content delivery network used by companies like Facebook, and Twitter perfectly adapted Scala for its most performance-demanding components, and AT & T uses Haskell for the security systems of its networks.

For most front-end developers, these languages ​​have a steep learning curve; However, many simpler and more accessible languages ​​have features of functional programming — at least the same Python with its standard library and its functions like map and reduce (which we will talk about later) and third-party libraries like Fn.py, or JavaScript that actively use methods of working with collections and having libraries like Underscore.js and Bacon.js.



Functional programming, of course, may seem complicated, but we must remember that it was created not only for doctors of science, adherents of data science and systems architecture gurus. For most, the real benefit of the functional style is that the programs can be divided into smaller and simpler parts, more reliable and much more understandable at the same time. If you are a frontend developer working with data, especially if you use D3 , Raphael or similar libraries for visualization, then functional programming can be a serious weapon in your arsenal.

Finding a strict definition of functional programming is not easy, and most of the literature relies on statements like "well, there functions are first-class objects" and "side effects disappear." If this has not yet torn your brain to pieces, then closer to theory, functional programming is often explained in terms of lambda calculus (although some argue that functional programming is, in principle, just mathematics), but please do not worry. In fact, it is enough for a beginner to understand only two ideas in order to use functional programming for their daily tasks (and without any lambdas!).

First, with the functional approach, the data must be unchanged (immobile), which sounds complicated, but it just means that they cannot change. At first glance, this may look strange (after all, who needs a program that does not change anything?), But in fact you just constantly create new data structures instead of changing existing ones. For example, if you need to do something with an array, you create a new array with new values ​​instead of changing the old array. Just the same!

Secondly, programs in a functional style should not have a state, which, in general, means that they should be performed as if no one had done anything before them, and without information about what could or could not happen in the program previously (we can say that the program without a state does not take into account its past). Together with immutability, this allows us to perceive each function as if it works in a vacuum, blissfully slaughtering everything else that is in the program, besides other functions. It also means that the function works only with data transmitted as parameters, and therefore can perform its work independently of some external values.

Data immutability and lack of state are the basis of functional programming, and it is extremely important to understand them, but don’t worry if everything is still vague for you. By the end of the article you will understand the essence of these principles, and I promise that the beauty, accuracy and power of functional programming will turn your code into a bright, brilliant, gnawing rainbow data. But for now, let's start with simple functions that return data (or other functions), and then combine them to perform more complex tasks.

For example, suppose we have some response from the API:

 var data = [{ name: "Jamestown", population: 2047, temperatures: [-34, 67, 101, 87] }, { name: "Awesome Town", population: 3568, temperatures: [-3, 4, 9, 12] }, { name: "Funky Town", population: 1000000, temperatures: [75, 75, 75, 75, 75] }]; 

If we want to use the graphic library to compare the average temperature and population, we need to write a little code that prepares the data for visualization. Let our library for plotting graphs waiting for the input array x - and y coordinates, like this:

 [ [x, y], [x, y] // ...  .. ] 

where x is the average temperature, and y is the population.

Without functional programming (or without its application, i.e. in the imperative style ), our code would look something like this:

 var coords = [], totalTemperature = 0, averageTemperature = 0; for (var i = 0; i < data.length; i++) { totalTemperature = 0; for (var j = 0; j < data[i].temperatures.length; j++) { totalTemperature += data[i].temperatures[j]; } averageTemperature = totalTemperature / data[i].temperatures.length; coords.push([averageTemperature, data[i].population]); } 

Even in this example sucked from the finger is not easy to keep track of everything. Well, let's see how to make everything clearer.

The functional approach is always characterized by the search for simple, frequently repeated actions that can be distinguished in a function. Then you can do more complex actions by calling these functions in a certain order (which is also called composition of functions ) - I’ll soon focus on this in more detail. We define what needs to be done to bring the source data into the format required by the library. Looking at the code at the edge of the eye, the following actions can be highlighted:

We will write a function for each of these actions, and then compose a program of these functions. At the beginning, functional programming can be a bit confusing, and you will probably be tempted to fall back on your old imperative habits. To prevent this from happening, I’ll provide below a list of the main rules to ensure that you follow the best practices:

Well, let's start the development. Step one - add each number to the list. Let's make a function that takes a parameter - an array of numbers, and returns some data.

 function totalForArray(arr) { //   return total; } 

So far so good, but ... how can we get access to each element of the list if we cannot cycle around it? Say hello to your new mate - recursion! When you use recursion, you create a function that calls itself if the special exit condition is not fulfilled - in this case, the current value is simply returned. It may not be clear, but look at this simplest example:

 //  ,      —     function totalForArray(currentTotal, arr) { currentTotal += arr[0]; //    JavaScript-:    Array.shift, //          ,     var remainingList = arr.slice(1); //    ,   ()   , //  currentTotal —   if (remainingList.length > 0) { return totalForArray(currentTotal, remainingList); } // ,     —       currentTotal else { return currentTotal; } } 

Caution: Recursion makes the code more readable and natural for programming in a functional style. However, in some languages ​​(including JavaScript), problems will arise if the program makes too many recursive calls (at the time of writing the article “too many” - that's about 10,000 calls in Chrome, 50,000 calls in Firefox and 11,000 calls in Node.js). Detailed parsing goes beyond the scope of this article, but the bottom line is that at least until the final release of ECMAScript 6, JavaScript will not support something called “tail recursion” - a more efficiently organized form of recursion. This is a complex and infrequent topic, but nevertheless it is useful to know about it.

Now back to our problem - we need to calculate the total temperature over the array of temperatures in order to then calculate the average. Now, instead of traversing an array in a loop, we can simply write:

 var totalTemp = totalForArray(0, temperatures); 

If you are a perfectionist, you can say that the totalForArray function totalForArray also be broken down into functions. For example, the addition of numbers may occur elsewhere in the program and, therefore, must also be performed by a separate function.

 function addNumbers(a, b) { return a + b; } 

Now our totalForArray function looks something like this:

 function totalForArray(currentTotal, arr) { currentTotal = addNumbers(currentTotal, arr[0]); var remainingArr = arr.slice(1); if (remainingArr.length > 0) { return totalForArray(currentTotal, remainingArr); } else { return currentTotal; } } 

Excellent! Getting a single value through an array is so often found in functional programming that it even has its own name — convolution — you may have heard as a verb — something like “minimize the list to a single meaning.” JavaScript has a special method for doing this. There is a detailed description on the Mozilla Developer Network, but for our task an example is enough:

 //       ,   //          var totalTemp = temperatures.reduce(function(previousValue, currentValue) { //      currentValue   previousValue + currentValue, //    previousValue      return previousValue + currentValue; }); 

But wait, we already defined the function addNumbers , so we can use it:

 var totalTemp = temperatures.reduce(addNumbers); 

And since the bundle is so steep, let's separate it into a separate function, so that we can simply call it without having to remember all these intricate details.

 function totalForArray(arr) { return arr.reduce(addNumbers); } var totalTemp = totalForArray(temperatures); 

Oh, now the code is easy to read! By the way, methods like convolutions are present in most functional languages. These helper methods that perform actions on lists instead of messing around with cycles are called higher order functions .

Well, moving on; The next task in our list is to calculate the average. It is very simple.

 function average(total, count) { return total / count; } 

How far will we go to calculate the average of the entire array?

 function averageForArray(arr) { return average(totalForArray(arr), arr.length); } var averageTemp = averageForArray(temperatures); 

I hope you started to understand how the composition of functions works to perform more complex tasks. All this is possible due to following the rules listed at the beginning of the article, namely that functions must take parameters and return data. Conveniently.

Now we need to extract one property from each object in the array. Instead of showing examples of recursion, I will focus on the important and show another method built into JavaScript: map . This method is needed if you need to display an array of structures of one type in the list of structures of another type:

 //     — ,  , //     var allTemperatures = data.map(function(item) { return item.temperatures; }); 

All this is cool, but extracting a property from a collection of objects is something that is being done all the time, so we will write a function for that.

 //  —  ,    function getItem(propertyName) { //  ,   ,     . //    ,       . return function(item) { return item[propertyName]; } } 

Wow, we wrote a function that returns a function! Now we can pass it to the map method:

 var temperatures = data.map(getItem('temperature')); 

If you like the details, I’ll explain: the reason we can do this is that the JavaScript functions are “first class objects,” which means that you can use the function just like any other value. Although this can be done in many languages, this is one of the requirements for a language to be used for programming in a functional style. By the way, this is also the reason why you can do things like $('#my-element').on('click', function(e) { //... }) . The second parameter of the on method is a function, and when you pass functions as parameters, you use them just as you would use ordinary values ​​in imperative languages. Gracefully.

Finally, we wrap the call to the map method into our own function to make it more readable.

 function pluck(arr, propertyName) { return arr.map(getItem(propertyName)); } var allTemperatures = pluck(data, 'temperatures'); 

So, now we have a set of generic functions that can be used anywhere in the application and even in other projects. We can count the elements of an array, get average values ​​over the array and create new arrays, extracting properties from the lists of objects. Now, last but not least, let's return to the original problem:

 var data = [{ name: "Jamestown", population: 2047, temperatures: [-34, 67, 101, 87] }, { // ... }]; 

We need to convert an array of objects like the one above into an array of pairs x , y :

 [ [75, 1000000], // ... ]; 

where x is the average temperature, and y is the population. First, we extract the data we need:

 var populations = pluck(data, 'population'); var allTemperatures = pluck(data, 'temperatures'); 

Now calculate the list of averages. It must be remembered that the function that we pass to the map method will be called for each element of the array; the return value of the transferred function will be added to the new array, and this new array will be assigned to our variable averageTemps .

 var averageTemps = allTemperatures.map(averageForArray); 

So far, everything is OK, but now we have two arrays:

 // populations [2047, 3568, 1000000] // averageTemps [55.25, 5.5, 75] 

But we need only one array, so we will write a function to combine them. Our function will check that the element at index 0 from the first array is paired with the element at index 0 from the second array, and so on for the remaining indices from 1 to n (where n is the total number of elements of each of the arrays) [since in pure JavaScript is missing zip - approx. translator].

 function combineArrays(arr1, arr2, finalArr) { //     finalArr = finalArr || []; //         finalArr.push([arr1[0], arr2[0]]); var remainingArr1 = arr1.slice(1), remainingArr2 = arr2.slice(1); //    ,    if ((remainingArr1.length === 0) && (remainingArr2.length === 0)) { return finalArr; } else { // ! return combineArrays(remainingArr1, remainingArr2, finalArr); } }; var processed = combineArrays(averageTemps, populations); 

One-liners are fun:

 var processed = combineArrays(pluck(data, 'temperatures').map(averageForArray), pluck(data, 'population')); // [ // [ 55.25, 2047 ], // [ 5.5, 3568 ], // [ 75, 1000000 ] // ] 


Closer to life


Finally, consider a more realistic example, this time adding to our toolkit Underscore.js , a JavaScript library that provides many great support functions. We will receive data from the CrisisNET service to collect information about collisions and disasters, and visualize them using the amazing D3 library.

The goal is to give CrisisNET visitors a picture of all the types of information offered by the service. To do this, we will calculate the number of documents received through the API of the service corresponding to a certain category (for example, “physical violence” or “armed conflict”). So the user will be able to see how much information is available on those topics that seem to him the most interesting.

A bubble chart can be a great option because they are often used to represent the relative sizes of large groups of people. Fortunately, D3 supports a special type of diagram called pack . Well, let's make such a graph, and let it show the number of times that the specified category name appears in the response from the CrisisNET API.

Before we begin, it should be recalled that D3 is a complex library that requires separate study. Since this article focuses on functional programming, we will not waste time describing how D3 works. Do not worry - if you are not familiar with this library, you can always copy the code to disassemble it. Scott Murray’s D3 Tutorial is a great source of knowledge about this library.

First of all, make sure that we have a DOM element in which D3 can place the graph generated from our data.

 <div id="bubble-graph"></div> 

Now create our chart and place it in the DOM.

 //   var diameter = 960, format = d3.format(",d"), //    20  color = d3.scale.category20c(), //  ,       var bubble = d3.layout.pack() .sort(null) .size([diameter, diameter]) .padding(1.5); //   DOM SVG-,  D3     var svg = d3.select("#bubble-graph").append("svg") .attr("width", diameter) .attr("height", diameter) .attr("class", "bubble"); 

The pack object takes an array of objects of the following format:

 { children: [{ className: , package: "cluster", value: }] } 

CrisisNET API returns the following data format:

 { data: [{ summary: "Example summary", content: "Example content", // ... tags: [{ name: "physical-violence", confidence: 1 }] }] } 

We see that each document has a tag property ( tag ), and this property contains an array of elements, each of which has a name property that contains a name — that is what we need. We will have to count the number of times the name of each tag is found in the results returned by the CrisisNET API. To begin with, we extract the information we need using the pluck function already written.

 var tagArrays = pluck(data, 'tags'); 

Now we have an array of this format:

 [ [{ name: "physical-violence", confidence: 1 }], [{ name: "conflict", confidence: 1 }] ] 

But we need a single array of all tags. Hmm, flatten 's use the convenient flatten function from Underscore.js - it will extract values ​​from nested arrays and return us one common array without nesting.

 var tags = _.flatten(tagArrays); 

Well, now it's a little easier to work with our array:

 [{ name: "physical-violence", confidence: 1 }, { name: "conflict", confidence: 1 }] 

pluck , — .

 var tagNames = pluck(tags, 'name'); [ "physical-violence", "conflict" ] 

, .

— , , , . , — [ «» («arrays»), , , , — . ]. :

 [ ["physical-violence", 10], ["conflict", 27] ] 

, — , — , . , . , Underscore.js .

 var tagNamesUnique = _.uniq(tagNames); 

false - ( false , null , "" ..) y -, Underscore.js:

 tagNamesUnique = _.compact(tagNamesUnique); 

, , JavaScript — filter , , .

 function makeArrayCount(keys, arr) { //      return keys.map(function(key) { return [ key, //        arr.filter(function(item) { return item === key; }).length ] }); } 

, D3 pack , map .

 var packData = makeArrayCount(tagNamesUnique, tagNames).map(function(tagArray) { return { className: tagArray[0], package: "cluster", value: tagArray[1] } }); 

, D3 DOM- SVG — , , API CrisisNET.

 function setGraphData(data) { var node = svg.selectAll(".node") //      .data(bubble.nodes(data) .filter(function(d) { return !d.children; })) .enter().append("g") .attr("class", "node") .attr("transform", function(d) { return "translate(" + dx + "," + dy + ")"; }); //       node.append("circle") .attr("r", function(d) { return dr; }) .style("fill", function(d) { return color(d.className); }); //         node.append("text") .attr("dy", ".3em") .style("text-anchor", "middle") .style("font-size", "10px") .text(function(d) { return d.className }); } 

, setGraphData makeArray , API CrisisNET jQuery (, API- ). GitHub.

 function processData(dataResponse) { var tagNames = pluck(_.flatten(pluck(dataResponse.data, 'tags')), 'name'); var tagNamesUnique = _.uniq(tagNames); var packData = makeArrayCount(tagNamesUnique, tagNames).map(function(tagArray) { return { className: tagArray[0], package: "cluster", value: tagArray[1] } }); return packData; } function updateGraph(dataResponse) { setGraphData(processData(dataResponse)); } var apikey = //  API-  : http://api.crisis.net var dataRequest = $.get('http://api.crisis.net/item?limit=100&apikey=' + apikey); dataRequest.done(updateGraph); 

, ! , , !

, , . , , , . , — , !

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


All Articles