In many cases, this article explains the advantages of functional programming. I personally agree that functional programming has some benefits, but I do not advocate thorough functional programming, especially for the development of complex application logic. In many cases, this article explains the advantages of functional programming. I personally agree that functional programming has some benefits, but I do not advocate thorough functional programming, especially for the development of complex application logic.
JavaScript functional programming
In recent years, Functional Programming (Functional Programming) has become one of the hottest topics in the JavaScript community. Whether you appreciate this Programming concept or not, I believe you already know it. Even when functional programming was not popular in the past few years, I have found many in-depth practices for functional programming concepts in many large application code libraries. Function programming avoids the use of Shared State, Mutable Data, and Side Effects in software development projects ). In functional programming, the entire application is driven by data, and the state of the application flows between different pure functions. In contrast to imperative programming-oriented object-oriented programming, functional programming is more inclined to Declarative Programming, code is more concise and clear, more predictable, and more testable .. Functional Programming is essentially a Programming Paradigm (Programming Paradigm), which represents a series of basic definition principles used to build software systems. Other Programming paradigms include Object Oriented Programming and Procedural Programming ).
Pure Function
As the name suggests, pure functions often refer to those functions that output data only based on input parameters and do not produce any side effects. One of the best features of pure functions is the predictability of their results:
var z = 10;function add(x, y) { return x + y;}console.log(add(1, 2)); // prints 3console.log(add(1, 2)); // still prints 3console.log(add(1, 2)); // WILL ALWAYS print 3
In the add function, the z variable is not operated, that is, the value of z is not read or modified. It only returns the sum of x and y according to the input parameters. This add function is a typical pure function. If the add function involves reading or modifying the z variable, it will lose its purity. Let's look at another function:
function justTen() { return 10;}
For a function without any input parameters, if it is to be a pure function, the return value of the function must be a constant. However, it is better to define a constant as a fixed function that returns a constant, so there is no need to use a function, therefore, we can think that the vast majority of useful pure functions allow at least one input parameter. Let's look at the following function:
function addNoReturn(x, y) { var z = x + y}
Note that this function does not return any value. It does have two input parameters x and y, and then adds these two variables and assigns them to z, therefore, such a function can be considered meaningless. Here we can say that the vast majority of useful pure functions must have return values. In summary, pure functions should have the following special effects:
● Most pure functions should have one or more parameter values.
● A pure function must have a return value.
● The return values of pure functions with the same input must be consistent.
● Pure functions cannot produce any side effects.
Sharing status and side effects
Shared State can exist in the Shared scope (global scope and closure scope) or any variable, object, or memory space passed to the object attributes of different scopes. In object-oriented programming, we often share an object by adding attributes to other objects. The sharing status problem is that if developers want to understand the role of a function, they must learn more about the impact that the function may have on each shared variable. For example, if you need to save the user objects generated by the client to the server, you can use the saveUser () function to send a request to the server, pass the user information encoding, and wait for the server to respond. When you initiate a request, the user modifies the profile picture and triggers another updateAvatar () function and another saveUser () request. Normally, the server will first respond to the first request and modify the user information stored in the memory or database based on the change of USER Parameters in the second request. However, in some unexpected situations, the second request may arrive at the server first than the first request, so that the new profile selected by the user will be overwritten by the old profile picture in the first request. The user information stored on the server is shared, and Data Consistency disorder caused by multiple concurrent requests is also known as Race Condition ), it is also one of the typical problems caused by the sharing status. Another common problem with the sharing status is that different calling sequence may trigger unknown errors, because operations on the sharing status are often time series dependent.
Const x = {val: 2}; const x1 = () => x. val + = 1; const x2 = () => x. val * = 2; x1 (); x2 (); console. log (x. val); // 6 const y = {val: 2}; const y1 = () => y. val + = 1; const y2 = () => y. val * = 2; // The function call sequence y2 (); y1 (); // the final result is also affected. log (y. val); // 5
Side effects refer to any observed application state changes that are not presented through the return value during function calls. common side effects include but are not limited:
● Modify attributes of any external variables or external objects
● Output logs in the console
● Writing files
● Initiate Network Communication
● Trigger any external process event
● Call any other function with side effects
In functional programming, we will try our best to avoid side effects and ensure that the program is easier to understand and test. Haskell or other functional programming languages usually use Monads to isolate and encapsulate side effects. When programming starts in most real application scenarios, it is impossible to ensure that all functions in the system are pure functions, however, we should try our best to increase the number of pure functions and separate the negative-side parts from pure functions, especially to abstract the business logic into pure functions, to ensure that the software is easier to expand, refactor, debug, test and maintain. This is why many front-end frameworks encourage developers to isolate user State management from component rendering and build loosely coupled modules.
Immutability
Immutable Object is the objects that cannot be modified after creation, and the Mutable Object is the objects that can still be modified after creation. Immutability (Immutability) is one of the core concepts of functional programming, ensuring the lossless nature of data streams during program running. If we ignore or discard the history of state changes, it is difficult for us to capture or reproduce some strange small probability problems. The advantage of using immutable objects is that you access any variable anywhere in the program, and you only have read-only permission, which means we no longer have to worry about unexpected illegal modifications. On the other hand, especially in multi-threaded programming, the variables accessed by each thread are constants, so the thread security can be fundamentally guaranteed. In summary, immutable objects can help us build simple and safer code.
In JavaScript, we need to figure out the difference between const and immutable. The variable name declared by const is bound to a memory space and cannot be allocated twice. It does not create a real immutable object. You can modify a property value of an object without modifying the point of a variable. Therefore, const creates a variable object. The most convenient way to create immutable objects in JavaScript is to call the Object. freeze () function, which can create an immutable Object layer.
const a = Object.freeze({ foo: 'Hello', bar: 'world', baz: '!'});a.foo = 'Goodbye';// Error: Cannot assign to read only property 'foo' of object Object
However, such objects are not completely immutable data. For example, the following objects are mutable:
const a = Object.freeze({ foo: { greeting: 'Hello' }, bar: 'world', baz: '!'});a.foo.greeting = 'Goodbye';console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);
As you can see above, the basic type attributes on the top layer cannot be changed. However, if the object type attributes, such as arrays, can still be changed. In many functional programming languages, the special unchangeable Data Structure Trie Data Structures is provided to implement a truly unchangeable Data structure, and attributes at any level cannot be changed. Tries can also use Structural Sharing to share unchanged object attribute values between new and old objects, thus reducing memory usage and significantly improving the performance of some operations. Although the language itself does not provide us with this feature in JavaScript, Tries features can be used through helper libraries such as Immutable. js and Mori. I have used both libraries, but I prefer Immutable. js in large projects. It is estimated that many people who are used to imperative programming will say: How should I program in a world without variables? Don't worry. Now let's consider when we need to modify the variable value: for example, modifying the attribute value of an object or modifying the value of a counter in a loop. In function programming, what corresponds to directly modifying the original variable value is to create a copy of the original value and assign it to the variable after modification. For another common loop scenario, such as the well-known for, while, do, and repeat keywords, we can use recursion in function programming to achieve the original loop requirements:
// Simple cyclic structure var acc = 0; for (var I = 1; I <= 10; ++ I) acc ++ = I; console. log (acc); // prints 55 // implement the function sumRange (start, end, acc) {if (start> end) return acc; return sumRange (start + 1, end, acc + start)} console. log (sumRange (1, 10, 0); // prints 55
Note that in recursion, the start variable corresponds to variable I. Each time the value is added with 1 and acc + start is passed to the next recursive operation as the current value. In recursion, no old variable values are modified, but new values are calculated and returned based on the old values. However, if you really want to convert all iterations into recursive writing, it is estimated that this will inevitably be affected by the obfuscation of the JavaScript language, in addition, iterative thinking is not so easy to understand. In Elm, a language specifically for functional programming, the syntax is much simpler:
sumRange start end acc = if start > end then acc else sumRange (start + 1) end (acc + start)
The record of each iteration is as follows:
sumRange 1 10 0 = -- sumRange (1 + 1) 10 (0 + 1)sumRange 2 10 1 = -- sumRange (2 + 1) 10 (1 + 2)sumRange 3 10 3 = -- sumRange (3 + 1) 10 (3 + 3)sumRange 4 10 6 = -- sumRange (4 + 1) 10 (6 + 4)sumRange 5 10 10 = -- sumRange (5 + 1) 10 (10 + 5)sumRange 6 10 15 = -- sumRange (6 + 1) 10 (15 + 6)sumRange 7 10 21 = -- sumRange (7 + 1) 10 (21 + 7)sumRange 8 10 28 = -- sumRange (8 + 1) 10 (28 + 8)sumRange 9 10 36 = -- sumRange (9 + 1) 10 (36 + 9)sumRange 10 10 45 = -- sumRange (10 + 1) 10 (45 + 10)sumRange 11 10 55 = -- 11 > 10 => 5555
High-order functions
Functional Programming tends to reuse a series of common pure functions to process data, while Object-Oriented Programming encapsulates methods and data into objects. These encapsulated methods are not highly reusable and can only act on certain types of data. They can only process the data type of the instance of the object. In functional programming, data of any type is treated equally. For example, the map () function allows developers to pass in function parameters to ensure that it can act on objects, strings, and numbers, and any other types. In JavaScript, functions are also first-class citizens. That is, we can process functions like other types, assign them to variables, pass them to other functions, or use them as function return values. A Higher-Order Function is a Function that can accept a Function as a parameter and return a Function as a returned value. High-order functions are often used in the following scenarios:
● Use callback functions, Promise, or Monad to abstract or isolate actions, functions, and any asynchronous control flow
● Build tool functions that can act on generic data types
● Function reuse or creation of Corey Functions
● Compound functions that combine multiple input functions and return these functions
The application of a typical high-order function is a composite function. As developers, we naturally do not want to repeat building and testing code with some of the same parts over and over again, we have been looking for a suitable method that only needs to write the code once and how to reuse it to other modules. Code reuse sounds attractive, but it is hard to implement in many cases. If you write code that is too biased towards a specific business, it will be difficult to reuse it. If you write every piece of code too broadly, it is difficult for you to apply the Code to specific business scenarios, and you need to write additional connection code. What we really look for is to find a balance between the specific and general, so that we can easily write short, concise, reusable pieces of code, in addition, these small code slices can be quickly combined to solve complex functional requirements. In Function programming, functions are the most basic code block that we can target. In Function programming, the combination of basic blocks is the so-called Function Composition ). Take the following two simple JavaScript Functions as an example:
var add10 = function(value) { return value + 10;};var mult5 = function(value) { return value * 5;};
If you are used to using ES6, you can use Arrow Function to reconstruct the above Code:
var add10 = value => value + 10; var mult5 = value => value * 5;
It seems much refreshed now. Next we will consider a new function requirement. We need to build a function. First, add the input parameter to 10 and multiply it by 5, we can create a new function as follows:
var mult5AfterAdd10 = value => 5 * (value + 10)
Although the above function is also very simple, we still need to avoid writing any function from scratch, which will also let us do a lot of repetitive work. We can build new functions based on the add10 and mult5 functions above:
var mult5AfterAdd10 = value => mult5(add10(value));
In the mult5AfterAdd10 function, we have already built on the add10 and mult5 functions, but we can achieve this in a more elegant way. In mathematics, we think that f branch g is the so-called Function Composition, so 'f branch g can be considered as equivalent to f (g (x )), we can also reconstruct the above mult5AfterAdd10 based on this idea. However, JavaScript does not support native Function Composition. In Elm, we can use the following method:
add10 value = value + 10mult5 value = value * 5mult5AfterAdd10 value = (mult5 << add10) value
Here < <操作符也就指明了在elm中是如何组合函数的,同时也较为直观的展示出了数据的流向。首先value会被赋予给add10,然后add10的结果会流向mult5。另一个需要注意的是,(mult5 << add10)中的中括号是为了保证函数组合会在函数调用之前。你也可以组合更多的函数:< p>
f x = (g << h << s << r << t) x
In JavaScript, you may need to use the following recursive call to implement this function:
g(h(s(r(t(x)))))