Recently, when chatting with a friend who is a technology maker, I found that I couldn't explain the idea of functional programming clearly, so I made a review
1, The function is "first class citizen"
It is often heard that in JavaScript, a function is a "first-class citizen". What does this mean?
In programming language, the first-class citizen can be used as a function parameter, a function return value, or a variable -- Christopher Strachey
In fact, in many traditional languages (such as before C and JAVA 8), functions can only be declared and called, and cannot be used as parameters like strings
Functions in JavaScript are equal to other data types, which is the premise of functional programming
2, Pure functions
Now for formal contact function programming, let's first look at a simple requirement:
There's a bunch of user information
const arr = [
{name: 'Zhao Xin', gender: 1, age: 25, high: 176, weight: 62},
{name: 'Ash', gender: 2, age: 23, high: 161, weight: 46},
{name: 'Ali', gender: 2, age: 27, high: 182, weight: 53},
{name: 'Galen', gender: 1, age: 27, high: 175, weight: 78},
{name: 'Warwick', gender: 1, age: 42, high: 169, weight: 70},
{name: 'Annie', gender: 2, age: 16, high: 153, weight: 43},
{name: 'kalmar ', gender: 2, age: 40, high: 168, weight: 48},
{name: 'Fitz', gender: 0, age: 52, high: 163, weight: 50},
{name: 'Yasuo', gender: 1, age: 35, high: 177, weight: 65},
{name: 'Ruiwen', gender: 2, age: 33, high: 172, weight: 52},
]
Write a function to filter user information, count how many men are over 18 years old, and record their height and name
Maybe you would write:
const male = {
count: 0,
list: [],
};
const MIN_AGE = 18;
const Count = (arr) => {
for (const item of arr) {
if (
!item
|| +item.age < +MIN_AGE
|| `${item.gender}` !== '1'
) { continue }
male.count++;
male.list.push({
name: item.name,
high: item.high,
});
}
}
It seems that there is no problem with the sub sub, and we will write such functions in our work
But the above MIN_AGE and male are external variables (or global variables)
When we write business, we can't find any fault in this way of writing, but they are not pure functions
Pure functions have two characteristics:
1. Independent of external state, the same input will always get the same output;
2. There is no side effect and the input parameter or global variable will not be modified. / / splace is talking about you!
In the above example, if Count(arr) is executed several times in a row, there will be a problem:
If you follow the standard of pure function, you can change it to this way:
const Count = (arr, min) => {
// Create a local variable
const res = {
count: 0,
list: [],
};
for (const item of arr) {
if (
!item
|| +item.age < +min // Use input parameters instead of global variables
|| `${item.gender}` !== '1'
) { continue }
res.count++;
res.list.push({
name: item.name,
high: item.high,
});
}
// Return results
return res;
}
After this adjustment, the function is completely self-sufficient, and we can also clearly know what parameters the function depends on
But there seems to be nothing special about this adjustment. If we change the screening criteria to women who weigh less than 50kg, this function needs to be adjusted a lot
Don't worry. We're just at the beginning. Next, we'll build a business function that is easy to maintain and readable
3, Curry
In fact, the above example adopts the idea of imperative programming, focusing on how to realize the current requirements step by step
Functional programming is more like a factory assembly line composed of one processing station. It can also meet the requirements, but it focuses more on how to use the processing station
This processing station is currification. The concept of currification is very simple: convert a multi parameter function into a single parameter function called in turn
fun(a, b, c) -> fun(a)(b)(c)
We need to pay attention to the difference between currification and local call
A local call is a function that passes only a part of its parameters and returns a function to process the remaining parameters
fun(a, b, c) -> fun(a)(b, c) / fun(a, b)(c)
However, in actual work, the two concepts have little impact on the actual work, because they all use the curry functions provided by the tool library (such as Lodash, Ramda), and these curry functions usually satisfy both currification and local call
First of all, a simple example is used to understand corrilization. First, a summation function is declared
const sum = (x, y, z) => x + y + z;
Then we implement a simple curry function (usually we don't write the curry function ourselves, but directly use the functions provided by various tool Libraries curry function)
const curry = (fn) => {
return function recursive(...args) {
// If args.length >= fn.length It indicates that enough parameters are passed in, and the fn And return
if (args.length >= fn.length) {
return fn(...args);
}
// Otherwise, it indicates that not enough parameters are passed in, and a function is returned,Use this function to accept the new parameters passed later
return (...newArgs) => {
// Recursive call recursive function,And return
return recursive(...args.concat(newArgs));
};
};
};
Currification of sum function
const Sum = curry(sum); // -> [Function]
Sum(10)(11)(12); // -> 33
const Sum10 = Sum(10); // -> [Function]
const Sum10_11 = Sum10(11); // -> [Function]
Sum10_11(12); // -> 33
We can get the final result directly by using the sum after currification, or we can create two specific single parameter functions Sum10 and Sum10based on sum, which greatly enhances the flexibility of the original sum function
These functions are the basis of function combination.
4, Function composition
If a value needs to go through multiple functions to become another value, all intermediate steps can be combined into one function, which is function combination
const compose = (f, g) => x => f(g(x))
Take this simple version of the compose function as an example:
const f = x => x + 1;
const g = x => x * 2;
const fg = compose(f, g);
fg(1) // ----> ?
Don't use the console to debug, can you see whether the result of fg(1) is 3 or 4?
If you think about it, you will find a detail: the functions in the function combination are executed in reverse order, our input parameter is (f, g), but the actual execution order is g - > F
Now suppose we have four utility functions:
filter18(arr); // Returns data older than 18 from the array
filterMale(arr); // Filter the male data from the array and return the new array
pickNameHeight(arr); // Gets the name and height fields in the array and returns the new array
log(arr); // Print parameters
According to the idea of command programming, if you want to realize the initial demand of filtering user information through these four functions, you need to write as follows:
log(pickNameHeight(filterMale(filter18(arr))));
You're blinding, aren't you? Try using compose:
const fun = compose(log, pickNameHeight, filterMale, filter18);
fun(arr);
Now it's much clearer. We can see at a glance what the assembly line has done by entering the parameters
Moreover, we can get more and more flexible functions by combining different functions in different ways, which is exactly the charm of functional programming
Like the curry function, we usually use the compose function provided by various tool libraries directly
These libraries usually provide a pipe function, which is similar to compose function, but the execution order of pipe is opposite to compose function, and the input function will be combined from front to back
Now we have two powerful tools of functional programming: curry and compose. Let's go back to the first requirement
5, Actual combat
Again, we need to write a function to filter user information, count how many men are over 18 years old, and record their height and name
In fact, we only need to do three things. First, we need to filter out the data of people over 18 years old, then we need to filter out the data of men, and finally we need to get their height and name
1. To filter out data over 18 years old, we need to implement a tool function for size comparison
// Verify one of the objects key Is it greater than the critical value val
function porpGt(key, val, item) {
return item[key] > val
}
If we can corrify this function, we can get the 18-year-old tool function of filtering
const cPropGt = curry(porpGt); // porpGt(a, b, c) -> cPropGt(a)(b)(c)
const filter18 = cPropGt('age')(18); // cPropGt('age')(18)(item) -> filter18(item)
arr.filter(filter18); // return age Data greater than 18
2. Filter out men, which requires a tool function to judge equivalence
// Judge one of the objects key Is it equal to the critical value val
function porpEq(key, val, item) {
return `${item[key]}` === `${val}`
}
The same goes for currification, and you get the tool function to filter men
const cPropEq = curry(porpEq); // porpEq(a, b, c) -> cPropEq(a)(b)(c)
const filterMale = cPropEq('gender')(1); // cPropEq('gender')(1)(item) -> filterMale(item)
arr.filter(filterMale); // return gender Data equal to 1
3. Record height and name, need a tool function to extract value from object
// Extract multiple values from an object and return a new object
function pickAll(keys, item) {
const res = {};
keys.map(key => res[key] = item[key]);
return res;
}
Currify, and keep the name and high fields
const cPickAll = curry(pickAll);
const pickProps = cPickAll(['name', 'high']);
arr.map(pickProps); // Keep only name and high
After these three steps are completed, if the object-oriented writing method is adopted, it can be directly linked to call:
arr.filter(filter18)
.filter(filterMale)
.map(pickProps)
If a tool library is used, it usually has a tool function such as filter(), map(). Its function is the same as the filter and map of the data, but there are some differences in the way of calling
So when using the tool library, you can easily use function combinations:
const Count = compose(
map(pickProps),
filter(filterMale),
filter(filter18),
);
Count(arr);
If you need to adjust the filter conditions, you only need to modify the input parameters of the tool functions a little, generate new tool functions, and then combine them
6, Summary
Functional programming makes code clearer and easier to maintain
However, as can be seen from the above example, the imperative writing method only traverses once, while the functional programming writing method traverses three times
So I'd like to remind you that functional programming is not a panacea. Even in some situations with strict performance requirements, functional programming is not a proper choice
I think the relationship between imperative programming, object-oriented programming and functional programming is like that between cars, ships and airplanes
There is no absolute advantage or disadvantage between them. Maybe in most cases, the speed of the plane will be faster than that of the car, but in the mountains, the plane can not land safely
Learning more programming ideas is just mastering more skills, that's all.
reference material: