1. Basic concepts
Function composition
In computer science, function composition is the act or mechanism of combining several simple functions into one more complex function. The execution result of each function is passed as an argument to the next function, and the execution result of the last function is the result of the whole function.
In the figure below, the processing of a function can be thought of as a pipeline, where A represents the input value, b represents the output value, and FN represents the pipeline for processing data.
If the process (FN) becomes more complex, fn can be broken down into smaller units (F1, F1,3). A becomes the input value of f1; The output value m of F1 becomes the input value of F2; The output of F2, n, becomes the input of F3, and finally the output of B, which is the output of the whole process.
Associative law (associativity)
In mathematics, a representation containing more than two associative operators in which the order of operations does not affect the resulting value as long as the position of the operator is not changed.
Addition and multiplication are both combinable:
(x * y) * z === x * (y * z)
(x + y) + z === x + (y + z)
Copy the code
The combination of functions follows the associative law, which means that as long as the position of the function does not change, any combination takes precedence and does not affect the execution result.
compose(f,g,h) == compose(compose(f, g), h) == compose(f, compose(g, h))
Copy the code
2. Target use
- Improve code readability by avoiding onion nesting
In development, we often use such functions as f(g(h())). As a simple example, let’s say we want to do an arithmetic operation and find the square of 3 plus 5 times 2. There are actually three functions that need to be used:
// Add
const add = (a,b) = > a+b
const double = (x) = > x * 2
const square = (n) = > Math.pow(n,2)
square(double(add(3.5))) / / 256
Copy the code
This onion – like grammatical structure is very unreadable. If there are more nested relationships, it is almost difficult to discern and distinguish the calling relationships with the naked eye. In such scenarios, the syntax becomes simple and clear using function combinations.
const cmp = compose(square, double, multi)
const result = cmp(3.5) / / 256
Copy the code
- Combine functions of a single function to generate new functions of a particular function
The simplicity principle is helpful to improve the applicability and closure of functions. However, in the face of complex functions, convenient combinations of single function functions are a problem. Function combination can be the function of a single function, recombination can be relatively richer and more complete, better versatility of the new function, easy to use and reuse.
As a simple example, we often need to format our input. For example, the amount. You need to go through the process of removing closing Spaces, length restriction, and thousandth formatting.
const trim = (str) = > String.prototype.trim.call(str)
const limit = (str) = > String.prototype.substr.call(str,0.11)
const numberic = (str) = > String(str).replace(/ ((\ d {1, 3})? =(\d{3})+(? : $| \.) )/g."$1")
const format = compose(numberic,limit,trim)
format(10000) // 10,000
format(123000000) / / 123000000
format('123456789000000') / / 12345678900
Copy the code
- It is easy to maintain and reuse the complex function by disassembling the single function
On the contrary, multiple functions of a single function can be reintegrated into a complex function. On the other hand, a function with complex functions can be disassembled into a number of functions with a single function, which can be used separately or composed for recombination. This way, we can easily maintain and reuse.
3. Implementation principle
Let’s examine the compose implementation
First, compose is a higher-order function
- One or more functions are required as arguments
- The return value is also a function that accepts arguments
function compose(. funcs){
return function(){
// ...}}Copy the code
Secondly, Function composition is executed from right to left, and the result of each Function is passed as a parameter to the next parameter until all functions are completed. This operation of cumulative calculation results is very consistent with the application scenario of array.prototype. reduce function. The only difference is that the order of reduce operations runs from left to right. You can reverse the order with the reverse method or use the reduceRight method directly.
Reverse the order with reverse
function compose(. funcs){
return function(. args){
const start = funcs.pop()
return funcs.reverse().reduce((result, next) = >next.call(this,result), start.apply(this,args))
}
}
Copy the code
Using reduceRight
function compose(. funcs){
return function(. args){
const start = funcs.shift()
return funcs.reduceRight((result, next) = >next.call(this,result), start.apply(this,args))
}
}
Copy the code
Finally, the compose parameter validation needs to be perfected. If the parameter length is 0, there is no function to be combined, and an empty function whose return value is the parameter itself is returned. If there is only one function to combine, return it directly; The complete compose function is shown below.
function compose(. funcs) {
let len = funcs.length
if (len === 0) {
return (. args) = > args
}
if (len === 1) {
return (. args) = > funcs[0].apply(this, args)
}
return function (. args) {
const start = funcs.pop()
return funcs.reverse().reduce((result, next) = > next.call(this,result), start.apply(this,args))
}
}
Copy the code
Of course, you can also validate the parameter funcs type
function compose(. funcs) {
let len = funcs.length
for (const fn of funcs) {
if (typeoffn ! = ='function') throw new TypeError('Params must be composed of functions! ')}if (len === 0) {
return (. args) = > args
}
if (len === 1) {
return (. args) = > funcs[0].apply(this, args)
}
return function (. args) {
const start = funcs.pop()
return funcs.reverse().reduce((result, next) = > next.call(this,result), start.apply(this,args))
}
}
Copy the code
There are many ways to achieve, the following gives another several ways to achieve, can be interested in studying.
The implementation idea based on the for loop is relatively simple. The loop is executed, and the result is passed down as a parameter until the end of the loop.
function compose(. funcs) {
let len = funcs.length
if(len===0) {return (. args) = > args
}
if (len === 1) {
return (. args) = > funcs[0].apply(this, args)
}
return function(. input) {
let start = funcs.pop()
let result = start.call(this, input)
for (let fn of funcs.reverse()){
result = fn.apply(this, result)
}
return result
}
}
Copy the code
Here’s how compose is implemented in REdux. Reverse, or reduceRight, was not used to deal with the order of execution. Instead, anonymous functions are used to encapsulate cache execution from the outside in. When executing, the order becomes from inside out, cleverly handling the execution order.
function compose(. funcs) {
if (funcs.length === 0) {
return arg= > arg
}
if (funcs.length === 1) {
return funcs[0]}return funcs.reduce((a, b) = > (. args) = >a(b(... args))) }Copy the code
An enhanced version of Compose
Asynchronous programming is often used during development. Promise, Ajax requests, Async… Await, setTimeout and many other asynchronous programming methods bring great convenience to front-end development. Next we use async… Await, modify and augment the compose function to support asynchronous functions.
Reduce version of asynchronous compose
Note async… Problem with the usage of await
function asyncCompose(. funcs) {
let len = funcs.length
if (len === 0) {
return (. args) = > args
}
if (len === 1) {
return (. args) = > funcs[0].apply(this, args)
}
const start = funcs.pop()
return async function (. args) {
return await funcs.reverse().reduce(async (result, next) => next.call(this.await result), await start.apply(this, args))
}
}
Copy the code
for… Of version of asynchronous compose
function asyncCompose(. funcs) {
let len = funcs.length
if (len === 0) {
return (. args) = > args
}
if (len === 1) {
return funcs[0]}return async function (. input) {
let start = funcs.pop()
let result = awaitstart(... input)for (let fn of funcs.reverse()) {
result = await fn(result)
}
return result
}
}
Copy the code
Example verification:
let str = '123456789000000'
const sleep = (fn, time) = > new Promise((resolve) = > {
setTimeout(() = > resolve(fn()), time)
})
const trim = (str) = > String.prototype.trim.call(str)
const limit = (str) = > String.prototype.substr.call(str, 0.11)
const numberic = (str) = > String(str).replace(/ ((\ d {1, 3})? =(\d{3})+(? : $| \.) )/g."$1")
const asyncTrim = (str) = > sleep(() = > trim(str), 1000)
const asyncLimit = (str) = > sleep(() = > limit(str), 1000)
console.log(compose(numberic, limit, trim)(str));
(async() = >console.log(await asyncCompose(numberic, asyncLimit, asyncTrim)(str)))()
Copy the code
Pipe (Pipeline)
In functional programming, there is a concept very similar to Function Composition called pipelines. We can simply think of it as compose from left to right. The implementation is similar to that of Compose.
For a simple example, in the LoDash library pipeline corresponds to the flow method and compoe corresponds to flowRight. Take a look at its source code.
function flow(. funcs) {
const length = funcs.length
let index = length
while (index--) {
if (typeoffuncs[index] ! = ='function') {
throw new TypeError('Expected a function')}}return function(. args) {
let index = 0
let result = length ? funcs[index].apply(this, args) : args[0]
while (++index < length) {
result = funcs[index].call(this, result)
}
return result
}
}
function flowRight(. funcs) {
returnflow(... funcs.reverse()) }Copy the code
4. A bit of discussion on Pointfree
There has been a lot of discussion about the concept of Pointfree, and even the Chinese translation is not exactly conclusive.
Simple said.
Pointfree defines the process of processing data as a composition of operations independent of parameters. You don’t need the parameter that represents the data, you just need to combine some simple steps.
How to understand this sentence? I personally think it’s much clearer to understand functional programming from the nature of it. Functional programming is simply the process of dealing with functions. Process existing functions into more applicable, easier to maintain, and more automated functions. Currying and Partial applications are treatments for arguments; Composition and Pipeline are for the combination and separation of functions; Both types of function-specific programming have one thing in common: they focus on processing functions and generating new functions without explicitly declaring parameters.
For a simple example, the format function is generated by composing numberic,limit, and trim. Information about parameters is implicit in the processing of a function. Format takes arguments, which are passed down to the underlying function to process and return the final value.
const format = compose(numberic,limit,trim)
Copy the code
5. Reference materials
www.codementor.io/@michelre/u…
En.wikipedia.org/wiki/Functi…
Github.com/mqyqingfeng…
Github.com/sindresorhu…
Segmentfault.com/a/119000001…
www.ruanyifeng.com/blog/2017/0…