Practical JavaScript functional programming

Keywords: Javascript Programming Java less

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 Sum10 based 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:

Functional programming to the North

Introduction to functional programming

Posted by tserbis on Mon, 11 May 2020 03:10:42 -0700