JavaScript function currying

Subscribe to my newsletter and never miss my upcoming articles

Definition of Currying

Currying is a technique that turns a function that accepts m parameters into a function that accepts n parameters (0<n<m), and the function returns a new function that accepts the remaining parameters ...And so on, until the result is returned at the end.

Looking at this definition may be a little abstract, let's look at a simple example.

// Ordinary add function
function add(x, y) {
    return x + y
}

// Currying
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

In fact, it turns the x and y parameters of the add function into a function that receives x and then returns a function to process the y parameter.

So, what are the specific benefits of currying an extra layer of encapsulation?

Benefits of currying

1. Multiplexing parameters

// Normal regular verification string reg.test(txt)
// After function encapsulation
function check(reg, txt) {
return reg.test(txt)
}
check(/\d+/g,'test') //false
check(/[a-z]+/g,'test') //true
// After Currying
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('21212') // false

The above example is a regular check. Normally, it is enough to call the check function directly, but if we have to check whether there are numbers in many places, the first parameter does not actually change, but the second parameter changes. . After Currying, the input of the first parameter reg can be omitted, and it will be easier to type the code later.

2. Delayed operation

// Use reduce to implement multi-parameter version add
let add = function(...args){
return args.reduce(function(accumulator, currentValue) {
return accumulator + currentValue;
},0)
};
// A simple currying implementation
function currying(func) {
const args = [];
return function result(...rest) {
if (rest.length == 0) {
return func(...args);
} else {
args.push(...rest);
return result;
}
}
}
let sum = currying(add);
sum(1)(2)(3); //The sum operation is not actually performed
sum(4); //The sum operation is not actually performed
sum(); //Perform the sum

The above code allows the function to determine the parameters through currying. If there are parameters, they are saved, and the summation is not executed. The summation operation is not executed until the last step, which achieves the effect of delaying operation. Re

aders may ask, must currying be necessary for delayed operation? of course not. Delayed operation is not necessarily related to currying. In essence, delayed operation can be realized by returning a new function (closure) to the function. Here is just an example to illustrate that currying has this feature. By the way, the reduce and rest operators of ES6 are used in the code, which may affect reading comprehension a little, but after familiarizing with the grammar, you will find that the code written by these syntactic sugars is very concise.

Universal package

Given a function fn, design a general package (currying function) to act on this fn, so that this fn can support currying, how to achieve it? Ideas:

  • To make fn(a,b) equivalent to fn(a)(b), then fn(a) must return a function that accepts the parameter b and returns the same result as fn(a,b).

  • Design a currying function, which accepts the first parameter is the function fn to be curried, and the second to N parameters are the parameters originally to be passed to fn (this can be implemented by the rest operator).

  • Currying mainly focuses on "processing parameters". No matter how the form of parameter transfer is changed, the function after currying must process all the parameters of the previous function before it is qualified.

// Preliminary currying function, where fn is the function to be curried
var primaryCurrying = function (fn,...args) {
return function (...args2) {
// Combine all the parameters passed to this closure function with the previous args
var newArgs = args.concat(args2);
// Use apply as the parameter of fn and execute the merged parameter
return fn.apply(this, newArgs);
}
}
function add(a, b) {
return a + b;
}
var add1 = primaryCurrying(add, 1);
var result = add1(2);
console.log(result); //>> 3

Looking at the code example above, it's already a bit currying, but it doesn't meet the requirements. Because the new function add1 obtained after currying this code only supports passing "1 segment" parameters, if the parameters are passed in multiple segments, such as curry(a)(b), it will not be supported.

The pair of parentheses () on the right side of the function that can handle parameters is called "paragraph 1" in this book. If a function can have one more segment, we say that the function has the ability to have one more segment.

code show as below:

var curry = primaryCurrying(add);

var add = curry(1,2);
console.log(add);//>> 3

var add = curry(1)(2);
console.log(add); //>> TypeError: curry(...) is not a function

Look at the error message on line 7 of the code above. It is because curry(1) does not return a function. What should I do? If the parameters are not processed, just find a way to return to the function. In this case, recursion can be used to encapsulate a layer. We can use this primaryCurrying function as an auxiliary function (because it already supports 1 segment to handle parameters) to help us write real currying functions.

// Borrow the initial currying function before, so that the currying function fn has one more stage ability.
var primaryCurrying = function (fn,...args) {
return function (...args2) {
var newArgs = args.concat(args2);
return fn.apply(this, newArgs);
}
}
/**
* Design a real currying function.
* @param fn The function to be curried.
* @param length is used to record the length of the remaining parameters that fn should handle.
*/
function curry(fn, length) {
length = length || fn.length;
return function (...args2) {
//If the parameters originally to be passed to fn have not been passed yet
if (args2.length <length) {
//Merge parameters
var combinedArgs = [fn].concat(args2);
//Recursion, further currying. The primaryCurrying function is called here, and every time the function is called,
//You can add "1 segment" so that the remaining parameters can be processed until all the parameters that should be passed to fn have been processed.
return curry(primaryCurrying.apply(this, combinedArgs), length-args2.length);
}
//If all the parameters originally to be passed to fn have been passed, execute the fn function directly
else {
return fn.apply(this, args2);

This is actually on a preliminary basis, with a recursive call. As long as the parameters originally to be passed to fn have not been passed, the recursion will continue. Let's test the effect?

var fn = curry(function (a, b, c) {
    return [a, b, c];
});

var l=console.log;
l(fn("a", "b", "c")); //>> ["a", "b", "c"]
l(fn("a", "b")("c")); //>> ["a", "b", "c"]
l(fn("a")("b")("c")); //>> ["a", "b", "c"]
l(fn("a")("b", "c")); //>> ["a", "b", "c"]

Package with placeholder Placeholders can support passing parameters out of order, for example, the requirements are as follows:

var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", _, "c")("b"); //>> ["a", "b", "c"]

Here is a more powerful code implementation:

function curry(fn, args, holes) {
let length = fn.length;//The length of the formal parameter of fn
args = args || [];//The actual parameters of fn
holes = holes || [];//placeholder array
return function (...args2) {
let _args = args.slice(0),//store the combined parameters
_holes = holes.slice(0),
argsLen = args.length,//The length of the actual parameter of fn
holesLen = holes.length,
index = 0;
for (let i = 0; i <args2.length; i++) {
let arg = args2[i];
// To deal with situations like fn(1, _, _, 4)(_, 3), index needs to point to the correct subscript of holes
if (arg === _ && holesLen) {
index++;
if (index> holesLen) {
_args.push(arg);
_holes.push(argsLen-1 + index-holesLen);
}
}
// Deal with situations like fn(1)(_)
else if (arg === _) {
_args.push(arg);
_holes.push(argsLen + i);
}
// Deal with situations like fn(_, 2)(1)
else if (holesLen) {
// fn(_, 2)(_, 3)
if (index >= holesLen) {
_args.push(arg);
}
// fn(_, 2)(1) replace the placeholder with parameter 1
else {
_args.splice(_holes[index], 1, arg);
_holes.splice(index, 1);
}
}
else {
_args.push(arg);
}
}
if (_holes.length || _args.length <length) {
return curry.call(this, fn, _args, _holes);
}
else {
return fn.apply(this, _args);
}
}
}
///////////////////////start testing/////////////////////////
let _ = {'@@functional/placeholder':true};//Define a placeholder, here refer to the definition of Ramda
let fn = curry(function (a, b, c, d, e) {
console.log([a, b, c, d, e]);
});
// Verify that the output is all [1, 2, 3, 4, 5]
fn(1, 2, 3, 4, 5);
fn(_, 2, 3, 4, 5)(1);
fn(1, _, 3, 4, 5)(2);
//The above 3 are well understood, and the following 3 need to pay attention to understanding the rules
fn(1, _, 3)(_, 4)(2)(5);
fn(1, _, _, 4)(_, 3)(2)(5);
fn(_, 2)(_, _, 4)(1)(3)(5);

Currying performance Some performance problems of Currying, the author summarizes the following four points:

  • Some implementations are based on accessing the arguments object, which is usually a bit slower than accessing named parameters;
  • Some older browsers are quite slow in the implementation of arguments.length;
  • Using fn.apply(…) and fn.call(…) is usually slightly slower than calling fn(…) directly;
  • Creating a large number of nested scopes and closure functions will bring costs, both in terms of memory and speed. In fact, in most applications, the main performance bottleneck is manipulating DOM nodes. The performance loss of JavaScript is basically negligible compared to the performance loss of DOM operations, so currying can be used with confidence in most occasions.

A classic interview question

// Implement an add method so that the calculation result can meet the following expectations:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
///////////////////////////////////////
function add() {
// When executed for the first time, define an array to store all the parameters
var _args = Array.prototype.slice.call(arguments);
// Declare a function internally, save _args and collect all parameter values ​​using the feature of closure
var _adder = function() {
_args.push(...arguments);
return _adder;
};
// Utilize the feature of toString implicit conversion, implicit conversion when it is finally executed, and calculate the final value to return
_adder.toString = function () {
return _args.reduce(function (accumulator, currentValue) {
return accumulator + currentValue;
}, 0);
}
return _adder;
}
///////////////////////////////////////////////// //////
add(1)(2)(3).toString(); //>> 6
add(1, 2, 3)(4).toString(); //>> 10
add(1)(2)(3)(4)(5).toString(); //>> 15
add(2, 6)(1).toString(); //>> 9

Finally, currying is one of the important skills to realize functional programming. For more in-depth knowledge, you can view the content of the functional programming part of this book.

 
Share this