Please refer to my previous two articles for the concepts of pure functions and Currization. If you do not understand these two concepts, you may not understand the content of this article.

When we use pure functions and Currization, it’s easy to write onion code, h(g(f(x))), which is layer upon layer of code, like we’re taking the last element of an array and converting it to uppercase.

We can call the Reverse method of the array object to reverse the array, then call first to get the first element of the array, and then call toUpper to capitalize the first element.

const _ from 'lodash';

const array = ['a'.'b'.'c'.'d'];
_.toUpper(_.first(_.reverse(array)));
Copy the code

You can see that these method calls are layered on top of each other. This is onion code, and we can avoid this code by using a combination of functions.

Composition of functions allows you to recombine fine-grained functions to generate a new function. That is, you combine multiple functions into one new function.

For example, the above example needs to call the reverse,first, and toUpper functions. We can combine these three functions into one by combining them, and still pass in array array when calling. The processing result is unchanged. Function composition essentially hides the intermediate results of multiple function calls, such as reverse passing to first and first passing to toUpper.

Let’s look at the concept of function composition: if a function needs to be processed by multiple functions to get the final value, then we can combine these intermediate process functions into a new function.

A function is like a pipeline of data, and a combination of functions connects these pipes, allowing data to pass through multiple pipes to form the final result.

Function combinations are executed from right to left by default. For example, in the following code, we combine F1, F2, and f3. When fn is called, f3 is executed first, then F2, and finally f1, i.e. f3 is executed to F2, then F2 is executed to F1.

const fn = compose(f1, f2, f3);
const b = fn(a);
Copy the code

Now let’s show you how to use combinations of functions. If we want combinations of functions, we first need a function that can combine multiple functions into a single function. So let’s define this function.

ES6 reset(… ES6 reset(… ES6 reset(… ES6 reset(… Args), the function must return a new function, and the function we return must take one parameter, the input parameter, which we call value.

function compose (. args) {
    return function (value) {}}Copy the code

Notice that when we call the returned function, we get our final result. So the inside of this function should call the functions we passed in in turn, and execute from right to left.

Args is the function passed in, and we need to reverse it. After the reverse, we need to call each function in turn, and the return value of the previous function needs to be the argument of the next function.

Here, we choose the reduce method of array. This method receives a function as a parameter, and in the function, it receives two parameters. One is the return value of the previous execution, which we call ACC, and the second is the current traversal value of the array, which we call FN.

function compose (. args) {
    return function (value) {
        return args.reverse().reduce(function (acc, fn) = >{})}}Copy the code

Here acc receives the return value of the previous execution, which does not exist at the time of the first execution. We can set the initial value in the position of the second parameter of reduce, which is the value passed in.

function compose (. args) {
    return function (value) {
        return args.reverse().reduce(function (acc, fn) = >{
            
        }, value)
    }
}
Copy the code

Reduce is an array method. It takes a function as an argument, similar to how forEach is written, and it also iterates through the array. Unlike forEach, the function passed in takes two arguments. The first argument is the return value from the previous loop, and the second argument is the value from the array currently traversed.

Before because we here function composition is a function of the execution result after passed to a function, so choose the reduce, when the first function, we value as a parameter to a function is introduced into, and then will return to, the results of the second function, we can get the first function execution results acc, And pass it in as an argument to the second function, and so on.

function compose (. args) {
    return function (value) {
        return args.reverse().reduce(function (acc, fn) = >{
            return fn(acc);
        }, value)
    }
}
Copy the code

At this point we are done with our combinator function. Let’s change this code because it looks too messy. There are three returns overlapping each other.

const compose = (. args) = > value= > args.reverse().reduce((acc, fn) = > fn(acc), value)
Copy the code

So it looks a lot cleaner.

The conditions for a combination of functions

The combination of functions satisfies the associative law, which is the associative law in mathematics.

If we want to combine three functions into one function, we can combine the last two functions first, or we can combine the first two functions first, and the result will be the same. That’s the associative property.

For example, when we combine f, g and H, we can first combine f and G into a function and then combine them with H. We can also combine g and H into a function and then combine them with F. The following three methods are equivalent.

let t = compose(f, g, h);
compose(compose(f, g), h) === compose(f, compose(g, h)); // true
Copy the code

To demonstrate this, we use loDash’s flowRight composite function to combine toUpper, first, and Reverse. The function gets the last element of the array, uppercase.

const _ = require('lodash');
const f = _.flowRight(_.toUpper, _.first, _.reverse);
console.log(f(['a'.'b'.'c'])); // C
Copy the code

When we combine these three functions, we can combine the first two and then the third function. Through flowRight

const _ = require('lodash');
const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse);
console.log(f(['a'.'b'.'c'])); // C
Copy the code

Let’s combine the last two functions, again via flowRight.

const _ = require('lodash');
const f = _.flowRight(_.toUpper,_.flowRight(_.first, _.reverse));
console.log(f(['a'.'b'.'c'])); // C
Copy the code

It turns out that whether we combine the first two or the second two, we get the same result, which is the associative property, which is the same as the associative property in mathematics.

Debugging of function combinations

When we use a combination of functions, how do we debug if the result we execute is not what we expect?

For example, in the following code, we want to know when the result of the reverse execution is. We can add a log function to the reverse function and print it out.

const _ = require('lodash');

const log = (v) = > { // debug function, which does nothing and returns directly
    console.log(v); / / print v
    return;
}
const f = _.flowRight(_.toUpper, _.first, log _.reverse);
console.log(f(['a'.'b'.'c'])); // C
Copy the code

We can write an auxiliary function during debugging, and we can observe the execution result of each intermediate function through this auxiliary function.

So that’s the combination of functions.

PointFree

PointFree is a programming style whose concrete implementation is a combination of functions, which is more abstract.

The concept of PointFree is that we can define the process of data processing as a data-independent synthesis without using the parameters that represent the data, but as a synthesis of simple operation steps. Before using this pattern, we need to define some auxiliary basic operation functions.

The whole sentence is rather confusing, we can boil this sentence down to three points.

1. Point 1: There is no need to specify the data to be processed

2. Second point: only the process of synthesis operation is required

3. The third point: some auxiliary basic operation functions are needed in the synthesis operation.

In this example, we first combine some basic operations into a function without specifying the data to be processed. This is called the PointFree mode.

const _ = require('lodash');
const fp = require('lodash/fp');

const f = fp.flowRight(fp.join(The '-'), fp.map(_.toLower), fp.split(' '))
Copy the code

Let’s demonstrate the non-PointFree pattern and the PointFree pattern through an example.

Let’s look at the non-pointfree mode first, assuming we want to convert Hello World to something like hello_world.

In our traditional way of thinking, we would define a function to receive the data we want to process, and then we would process our data in that function to get the result we want, which is non-Pointfree.


function f (word) {
    return word.toLowerCase().replace(/\s+/.'_');
}
f('Hello World')
Copy the code

Using the PointFree model to solve this problem, we first define some basic operation functions and then make them into a new function without specifying the data we need to process during the composition process.

So let’s go back to the core of functional programming, which is abstracting operations into functions. The PointFree pattern is a synthesis of abstracted functions into a new function, and the synthesis is actually an abstraction process. In this abstraction we still don’t need to care about the data.

Let’s use the PointFree pattern to implement the above example.

First, we can convert the string to lowercase and then replace Spaces with underscores. If there are too many Spaces in the middle, we should use the re to match. So in this process we’re going to use two methods, one to convert to lowercase and one to string substitution.

const fp = require('lodash/fp');
fp.toLower; // Convert to lowercase method
fp.replace; // The string substitution method
Copy the code

Because we are using PointFree, we can merge the two procedures into a new function without specifying the data we need.

Let’s import loDash’s FP module first. Then we need to synthesize functions. Let’s define a flowRight combinator function in f equals FP.

So when we combine functions, the first thing we deal with is converting lowercase, we pass fp.tolower

const fp = require('lodash/fp');
const f = fp.flowRight(fp.toLower);
Copy the code

Then we use fp.replace, because flowRight is executed from right to left, we write it in front of fp.tolower.

We know that the method fp.replace takes three parameters. The first parameter is the pattern to match, that is, the value to be replaced, which can be a regular expression, the second parameter is the content to be replaced, and the third parameter is the string to process.

The methods provided in FP are already Currified, so we can just pass part of the argument and it will return a new function.

The first argument is the regular expression /\s+/g that matches the blank space. The second argument is underscore _. When we pass only two arguments, it returns a new function that accepts the data to be processed, so the function combination is complete.

const fp = require('lodash/fp');
const f = fp.flowRight(fp.replace(/\s+/g.'_'), fp.toLower);

f('Hello World'); // hello_world
Copy the code

We can see that when we are composing functions, we do not need to specify the data we are dealing with.