Currying functions

Recently encountered a strange problem, achieve the following effect:

sum(2, 3).result = 5;

sum(2, 3)(4, 5).result = 14;

sum(1, 2)(3).result = 6;

This topic looks very strange, in fact, it is a function currying plus a brain teaser, here to record the idea of this topic, if you do not understand currying, after reading the introduction of currying, you can first think about how to achieve this effect, anyway, the author thought for half an hour to slow down the mind turned out to be so simple, in the final analysis, the theory is sufficient, but less knowledge.

What is currying?

Currying is the transliteration of Currying. Currying is a technology that implements multi-parameter functions at the compile principle level. Currying - only passes a part of the parameters to the function to call it, and it returns a function to handle the remaining parameters…

Before talking about currying in JavaScript, let’s talk about what the original currying is and where it came from.

In the coding process, as coders, our essential job is to decompose complex problems into multiple programmable small problems.

Currying provides an implementation idea of recursion degradation for implementing multi-parameter functions - transforming a function that accepts multiple parameters into a function that accepts a single parameter (the first parameter of the original function), and returns a new function that accepts the remaining parameters and returns the result. In some programming languages (such as Haskell), the language feature of multi-parameter functions is supported through Currying technology.

For the JavaScript language, the concept of currying function that we usually talk about is not exactly the same as the concept of currying in mathematics and computer science.

Curried functions in mathematics and computer science can only pass one parameter at a time.

The curried function in our actual JavaScript application can pass one or more parameters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//normal function

function fn(a,b,c,d,e) {

console.log(a,b,c,d,e)

}

//generated curried function

let _fn = curry(fn);

_fn(1,2,3,4,5); // print: 1,2,3,4,5

_fn(1)(2)(3,4,5); // print: 1,2,3,4,5

_fn(1,2)(3,4)(5); // print: 1,2,3,4,5

_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5

Uses of currying

Parameters to reuse

Currying actually complicates the problem of short answers, but at the same time, we have more freedom when using functions. And the free handling of function parameters here is the core of currying. Currying essentially reduces generality and improves applicability.

Its purpose can also be called parameter to reuse, which means that the parameters you pass in front will have an impact on the function generated later.

For example, if we don’t use currying to encapsulate the check function, it looks like this

1
2
3
4
5
6
function checkByRegExp(regExp,string) {
return regExp.test(string);
}
checkByRegExp (/^ 1\ d {10} $/, '18642838455 '); // Verify phone number

checkByRegExp (/^ (\ w) + (\.\ w +) * @(\ w )+((\.\ w +)+)$/, ' [email protected] '); // check mailbox

Each number passed into this function has no effect on the following.

However, if the curried method is used:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Curried

let _check = curry(checkByRegExp);

//Generate tool function to verify phone number

let checkCellPhone = _check(/^1\d{10}$/);

//Generate tool function, verify mailbox

let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);

checkCellPhone ('18642838455 '); // Verify phone number

checkCellPhone ('13109840560 '); // Verify phone number

checkCellPhone ('13204061212 '); // Verify phone number

CheckEmail ('[email protected] '); // Check mailbox

CheckEmail ('[email protected] '); // Check mailbox

CheckEmail ('[email protected] '); // Check mailbox

The Regular Expression you passed in earlier will have an impact on the subsequent calculation results.

Delayed execution

Deferred execution is also an important usage scenario for Currying. The delayed evaluation feature of Currying requires the use of scope in JavaScript - to put it more colloquially, we need to use scope to save the last passed parameter. Similarly, bind and arrow functions can also achieve the same function.

In front-end development, a common scenario is to bind onClick events to tags, while considering passing parameters to bound methods.

Curried tool function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
Curry the function
* @param fn Original function to be curried
* @Param len The number of parameters required, the default is the number of parameters of the original function
*/
function curry(fn,len = fn.length) {
return _curry.call(this,fn,len)
}

/**
* Transfer function
* @param fn Original function to be curried
* @param len Number of parameters required
* @param args List of received arguments
*/
function _curry(fn,len,...args) {
return function (...params) {
let _args = [...args,...params];
if(_args.length >= len){
return fn.apply(this,_args);
}else{
return _curry.call(this,fn,len,..._args)
}
}
}

Answer

Currying

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sum() {
let result = 0;
Array.prototype.slice.call(arguments).forEach((cur) => {
result += cur;
});

function tempresult() {
Array.prototype.slice.call(arguments).forEach((cur) => {
result += cur;
});
tempresult.result = result;
return tempresult;
}
tempresult.result = result;
return tempresult;
}

Yes, this brain teaser is that function is also an object, just plug a result directly.

Thunk function (updated on 2020.06.03)

Recently, when looking at the Generator function, I found that there is a function called Thunk function, which looks very similar to the curried function. Record it here. The main content comes from阮一峰老师的博客, with minor modifications and comments.

Parameter evaluation strategy

Thunk functions have been around since the 1960s.

At that time, programming languages were just getting started, and computer scientists were still researching how to write compilers better. ** One point of contention is"求值策略", that is, when the parameters of the function should be evaluated. **

1
2
3
4
5
6
7
var

function

}

f(x

The above code defines function f first, and then passes it the expression x + 5. Excuse me, when should this expression be evaluated?

One view was"传值调用"(Call by value), that is, before entering the function body, the value of x + 5 (equal to 6) is calculated, and then the value is passed to function f. C language adopts this strategy.

1
2
3
f(x
//
f(6)

Another view was"传名调用"(Call by name), that is, pass the expression x + 5 directly to the function body and only evaluate it when it is used. The Hskell language adopts this strategy.

1
2
3
f(x
//
(x

** Call by value or call by name, which is better? The answer is that each has its own advantages and disadvantages. ** Call by value is relatively simple, but when evaluating the parameter, this parameter has not actually been used, which may cause performance losses.

1
2
3
4
5
function

}

f(3

In the above code, the first parameter of function f is a complex expression, but it is not used in the function body at all. Evaluating this parameter is actually unnecessary.

Therefore, some computer scientists prefer to “call by name”, that is, only evaluate at execution time.

Thunk

The “call by name” implementation of the compiler often puts the parameters into a temporary function, and then passes the temporary function to the function body. This temporary function is called a Thunk function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function

}

f(x

//

var

};

function

}

In the above code, the parameter x + 5 of function f is replaced by a function. Where the original parameter is used, just evaluate the Thunk function.

** This is the definition of a Thunk function, which is an implementation strategy of “call by name” to replace an expression. **

JavaScript

JavaScript language is called by value, and its Thunk function has a different meaning. ** In JavaScript language, Thunk function replaces not an expression, but a multi-parameter function, replacing it with a single-parameter version that only accepts a callback function as an argument. **

1
2
3
4
5
6
7
8
9
10
11
12
//
fs.readFile(fileName,

//
var
readFileThunk(callback);

var



};

In the above code, the readFile method of the fs module is a multi-parameter function with two parameters: the file name and the callback function. After the converter process, it becomes a single-parameter function that only accepts the callback function as a parameter. This single-parameter version is called Thunk function.

Any function, as long as the parameter has a callback function, can be written as a Thunk function. Here is a simple Thunk function converter.

1
2
3
4
5
6
7
8
9
var







};

Using the above converter, generate the Thunk function of fs.readFile.

1
2
var
readFileThunk(fileA)(callback);

Thunkify

Converter for production environment, recommended use Thunkify 模块

First the installation.

1
$

Use as follows.

1
2
3
4
5
6
7
var
var

var
read('package.json')(function(err,

});

Thunkified源码Very similar to the simple converter in the previous section.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function
























};

Its source code mainly has an additional checking mechanism.变量 called Make sure that the callback function only runs once. This design is related to the Generator function below. See the example below.

1
2
3
4
5
6
7
8
9
function



}

var
ft(1,
//

In the above code, since thunkify only allows the callback function to be executed once, only one line of result is output.

Generator

You may ask, what is the use of Thunk function? The answer is that it was really useless before, but ES6 has Generator function, and Thunk function can now be used for automatic Process management of Generator function.

Take reading a file as an example. The following Generator function encapsulates two asynchronous operations.

1
2
3
4
5
6
7
8
9
10
var
var
var

var




};

In the above code, the yield command is used to move the execution right of the program out of the Generator function, so a way is needed to return the execution right to the Generator function.

This method is the Thunk function, because it can return the execution right to the Generator function in the callback function. For ease of understanding, let’s first look at how to manually execute the above Generator function.

1
2
3
4
5
6
7
8
9
10
11
var

var
r1.value(function(err,






});

In the above code, the variable g is the internal pointer of the Generator function, indicating which step is currently executed. The next method is responsible for moving the pointer to the next step and returning the information of that step (value attribute and done attribute).

Looking closely at the above code, you can find that the execution process of the Generator function is actually passing the same callback function repeatedly into the value property of the next method. This allows us to use recursion to automate this process.

Thunk

The real power of the Thunk function is that it can automatically execute the Generator function. The following is a Generator actuator based on the Thunk function.

1
2
3
4
5
6
7
8
9
10
11
12
13
function









}

run(gen);

The run function of the above code is an automatic executor of the Generator function. The internal next function is the callback function of Thunk. Next function first moves the pointer to the next step of the Generator function (gen.next method), and then determines whether the Generator function ends (result.done property). If it does not end, pass the next function to the Thunk function (result.value property), otherwise exit directly.

With this executor, executing the Generator function is much easier. No matter how many asynchronous operations there are, just pass in the run function directly. Of course, the premise is that each asynchronous operation must be a Thunk function, that is, the yield command must be followed by a Thunk function.

1
2
3
4
5
6
7
8
var




};

run(gen);

In the above code, function gen encapsulates n asynchronous read file operations, which will be automatically completed as long as the run function is executed. In this way, asynchronous operations can not only be written like synchronous operations, but also executed in one line of code.

Thunk function is not the only solution for automatic execution of Generator function. Because the key to automatic execution is that there must be a mechanism to automatically control the flow of Generator function, receive and return the execution rights of the program. The callback function can do this, and so can the Promise object.

Encapsulating duplicate logic with curried functions (2020.10.10 update)

Recently read some vue source code, which for the platform judgment logic will be encapsulated in the curried function, through the incoming platform configuration, return a new function, so that each call to return the function can be omitted each time for the platform judgment.

This idea has been absorbed, coupled with recent work that requires encapsulating some utility classes for encapsulating CRUDs for specific datasets, such as user and userProfile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
const createRepositoryUtilForModelProfile = (mainModel, profileModel, foreignKey) => {
function selectSearchModels(where) {
const conditions = _.keys(where);
for (let i = 0; i < conditions.length; i++) {
if (mainModelKeys.indexOf(conditions[i]) > -1) {
return {
searchModel: mainModel.name,
supplementModel: profileModel.name
};
}
}
return {
searchModel: profileModel.name,
supplementModel: mainModel.name
};
}

const mainModelKeys = _.keys(mainModel.properties);
const profileModelKeys = _.keys(profileModel.properties);

const createUtil = async function(data, operator) {
let savedMain = await create({
model: mainModel.name,
data: _.pick(data, mainModelKeys),
operator });

const profile = await create({
model: profileModel.name,
data: _.pick(data, profileModelKeys),
operator
});
return _.merge(savedMain, profile);
};

const findUtil = async function (where, pageNumber, pageSize, fields) {
const { searchModel, supplementModel } = selectSearchModels(where);
let skip = _.toNumber(pageNumber - 1) * _.toNumber(pageSize);
let mainList = await find({
model: searchModel,
where,
skip: skip >= 0 ? skip : 0,
limit: _.toNumber(pageSize),
fields
});
for (let i = 0; i < mainList.length; i++) {
let supplement = await findOne({
model: supplementModel,
where: { [`${foreignKey}`]: mainList[i][`${foreignKey}`] },
fields });
_.merge(mainList[i], supplement);
}
return mainList;
};

const findOneUtil = async function(where, fields) {
const { searchModel, supplementModel } = selectSearchModels(where);
let main = await findOne({ model: searchModel, where, fields });
if (main) {
let supplement = await findOne({
model: supplementModel,
where: { [`${foreignKey}`]: main[`${foreignKey}`] },
fields });
return _.merge(main, supplement);
}
return main;
};

const updateAllUtil = async function(data) {
let updated = await updateAll({
model: mainModel.name,
where: { [`${foreignKey}`]: data[`${foreignKey}`] },
data: _.pick(data, mainModelKeys)
});
let profile = await updateAll({
model: profileModel.name,
where: { [`${foreignKey}`]: data[`${foreignKey}`] },
data: _.pick(data, profileModelKeys)
});
return Math.max(updated, profile);
};

return {
findUtil,
findOneUtil,
createUtil,
updateAllUtil
};
};

Reference link:

https://juejin.im/post/5af13664f265da0ba266efcf#heading-1

https://juejin.im/post/5d2299faf265da1bb67a3b65