preface

Recently, I have not written much code, but I have read several books, one of which is more interesting about functional programming, so I sorted out the essence of my reading notes and shared them, which can be recorded, so that the students who have not known it can easily understand it.

  • Functional Programming — Higher-order functions and Arrays (not yet written, see feedback 😂)
  • Functional Programming — Currization and Partial Functions

Everything in this article is implemented using ES6 code, and most browsers currently have almost complete support for ES6 syntax.

Nothing personal about anyone

There is also a very interesting place in writing this article, because when I saw the concept of partial function, I also looked up some information and then read a few articles while reading, and the result was a little disappointed, as for why disappointed? Let’s look for an article about partial functions in Nuggets:

Again, since I wrote it in Denver, I picked up a random article (top of the list), but it wasn't directed at any of the big guys, I read several articles and basically there were problems, just using the Nuggets example. There is no innuendo here. I think people who see and like the code in the last post will learn and remember it by heart, but if it's a piece of code that has problems, it's not so appropriate. Of course, I think the author's level, must know the problem, but did not write out, so HERE I put the problem of supplementary explanation.Copy the code

The code is still pretty nice, but let’s use the code to actually type a paragraph in the console to apply it:

As you can see, only the first result is correct, and all subsequent results are wrong. Of course, the author should be aware of such a simple problem, but in the original text, there is only one function encapsulation and call, so of course there is no problem. Again, not for anyone, just starting from personal experience, the content of the written at the very least not a problem, because if people see the study and application, which happens to be in business, really useless lodash encapsulated function, but after watching the modeled on written, so it is conceivable that the final will certainly out of the question.

Well, that’s the end of the episode, and I’m going to use my understanding to talk to you about two very important concepts in functional programming: Currization and partial functions.

Currie,

In fact, most of the concepts in JS are difficult to understand, sometimes you can learn faster than using a real example.

/** * a simple currization */
const add = (a, b) = > a + b;
const firstCurryAdd = a= > b= > a + b;
/ / execution
add(2.3); / / 5
firstCurryAdd(2) (3); / / 5
Copy the code

Currization definition

As you can see from the example above, what currization does is we had a function add that takes two arguments. Now we want to change it to a function that takes one argument in turn, and returns the result of two arguments.

[Definition] : Function corrification refers to the process of converting a multivariate (multi-parameter) function into a nested unary function.

In fact, careful analysis, can be very obvious, in fact, is the use of JS closure to achieve.

Kerrization implementation

So now we’re going to look at how to implement a function called Currization, and the implementation is, of course, by definition, essentially using closures, passing one argument at a time and returning a function that takes one argument at a time.

Implementation one: easy version

/** * Simple version of the implementation of currization */
const currySimple = fn= > x= > y= > fn(x, y);

/ / application
const curryAdd2 = currySimple(add);
curryAdd2(2) (3); / / 5
Copy the code

As you can see from above, a very simple implementation of the binary function Coriolization, well read right, just a binary function Coriolization, so it is called a simple version. We encounter not only two parameters in our daily development, but three, four or more. We can’t define many Coriolization functions as follows:

// Currization of binary functions
const curry2 = fn = x= > y= > fn(x, y);

// Currization of ternary functions
const curry3 = fn = x= > y= > z= > fn(x, y, z);

// The quaternion function is currified....Copy the code

The above code does not conform to the design specification, therefore, the above scheme is not a general scheme, we will explore the full version of the Corrification scheme.

Implementation two: The full version

I’m not going to do it step by step, because you’re not a beginner, but I’m going to put a comment in there, so you can feel free to read it.


/** * Full version of Currization */
const curry = fn= > {
  // First of all, the argument that a Currification takes must be a function, because it is a currification operation
  if (typeoffn ! = ='function') {
    throw Error('No function provided');
  }
  // The function returned here cannot be an anonymous function, because there may be some currification inside the function, so give it a name
  return function curryInnerFn(. args) {
    /** * There are two things to note here * first: if the currently called function receives a parameter less than the original function argument, then the parameter currification operation is performed, less than the function can not execute the result * second: You can get the length of a function's argument list via fn.length, which is equivalent to arguments.length */ inside the function
    if (args.length < fn.length) {
      // We return a function because we need to continue currization
  		return function() {
        // This returns an anonymous function of the ES5 form, since arguments are used for the merge arguments below, arrow functions have no arguments
        return curryInnerFn.apply(null, args.concat(
          /** ** if the parameter is smaller than the parameter of fn, the parameter will be returned after the curryification function. ** * if the parameter is smaller than the parameter of fn, the parameter will be returned after the curryification function. Function closure (args); function closure (args); function closure (args); Arguments is an array-like object to be converted to an array */
          [].slice.call(arguments))); }}If the number of arguments received by the currization function is the same as (or greater than) the number of arguments received by the original function, the original function returns the result
  	return fn.apply(null, args); }}Copy the code
The internal return uses the ES5 definition function form. Thank you for proposing the complete ES6 writing way in the commentsCopy the code

[Key points of Currization] : From the last section of code of Currization realization, we know that in fact, the essence of the final execution of the currization function is to wait until the parameter is received, and then pass the parameter to the original function and call the original function to obtain the result.

Parse it step by step

So just in case you don’t understand the above code, let’s go through it one by one with a function of three variables.

// Add three numbers
const add3 = (x, y, z) = > x + y + z;

/ / curry
const curryAdd3 = curry(add3);

/ / application
curryAdd3(1) (2) (3); / / 6
curryAdd3(1.2) (3); / / 6
Copy the code
  • The first step:const curryAdd3 = curry(add3);

What happens when you execute this line of code, which returns a function called curryInnerFn(… args)

  • The second step:curryAdd3(1)

Args. Length < fn.length (add3); args. Length < fn.length (add3);

curryAdd3(1) = function() {
    return  curryAdd3.apply(null[1].concat([].slice.call(arguments)));
}
Copy the code
  • Step 3:curryAdd3(1)(2)

The return from the previous step is clear. We pass in an argument 2, and the code looks like this:

curryAdd3(1) (2) = = =function(2) {
    return curryAdd3.apply(null[1].concat([].slice.call([2))); } => curryAdd3.apply(null[1.2]);
Copy the code

Args = [1, 2] < fn.length = 3, so repeat step 2.

  • Step 4:curryAdd3(1)(2)(3)

Apply (null, [1, 2, 3]), args. Length = 3 === fn.length = 3. Fn. apply(null, args); fn.apply(null, args);

  • Step 5:fn.apply(null, args) ===> add3(1, 2, 3)

Return the original function by assembling the received parameters into an array, and execute the original function, that is, curryAdd3(1)(2)(3) === add3(1, 2, 3), and then get the execution result of the original function, and at the same time complete the kerrization operation of the function.

Coriolization applications

So we’re done with the definition and the implementation, and there’s really nothing there. It’s all very basic applications. JS concept this thing a look to understand, but if not used in fact, forget is also very fast, because usually we do not often use Currie and partial functions, so lead to these two concepts we do not understand, so next we will talk about application scenarios.

In fact, from the implementation of Currie, it can be clearly seen that it is to transform a multi-parameter function into multiple unary functions, which means that it can be used to encapsulate fixed parameters.

[Key points] : Fixed parameters should be on the left side of the parameter list, that is, in front of it, so as to achieve currified encapsulation.

Case 1: Membership system

For example, each major website has membership system, different members have different benefits, for example, we often go to the barber shop, the shopkeeper will let us do a variety of membership charge money upgrade and so on. Suppose a store has silver/gold/platinum/diamond membership, the specific discount is as follows:

General => no discount silver => 9 discount gold => 8 discount platinum => 7 discount diamond => 6 discountCopy the code

Next, because of the existence of discounts and members, our function to calculate consumption can be designed as follows:

// The user actually consumes
const realConsume = (price, discount) = > {
    return price * discount;
}
Copy the code

In the end, when we calculate it, it looks like this:

// Suppose 10 people consume 5 silver and 5 gold
// Silver members actually spend money
realConsume(100.0.9);
realConsume(200.0.9);
realConsume(300.0.9);
realConsume(400.0.9);
realConsume(500.0.9);
// The actual consumption of gold members
realConsume(100.0.8);
realConsume(200.0.8);
realConsume(300.0.8);
realConsume(400.0.8);
realConsume(500.0.8);
Copy the code

The above code looks fine, and I’m sure most developers do it this way, so let’s change it with A Currization. Before the transformation, let’s talk about the problem first. It can be obviously seen that the discount of members is fixed, that is to say, 100 silver members spend different money. When calculating, the second parameter is transferred to a fixed discount of 0.9. That is to say, we can kerrize the original function and encapsulate the discount parameter as follows:

// Calculate the actual consumption
const realConsume = (discount, price) = > {
    return discount * price;
}

// Packaged silver -- actual consumption
const silverRealConsume = curry(realConsume)(9.);
// Encapsulated gold -- actual consumption
const goldRealConsume = curry(realConsume)(8.);

/ / application
silverRealConsume(100);
silverRealConsume(200);
silverRealConsume(300);
goldRealConsume(100);
goldRealConsume(200)
Copy the code

The above code uses The Cremation to encapsulate the function and simplify the payment scenario. As some of you may have said, this is actually not simplifying or even increasing the code. So what are the benefits of encapsulation?

First, because of the simplicity of the scenario, it feels like there’s more code, but as usage increases, the encapsulated code becomes easier to read and maintain.

Second, imagine how many parameters you would have to change in the code if we changed our silver membership from 10% discount to 88% discount with the previous usage, and we only had to change it once after currization. Of course, if the code is well written, using variables instead of constants works just as well, but from a design standpoint, encapsulation makes more sense.

In the end, all roads lead to Rome, and there are probably an infinite number of implementations that can make code run, but why not choose a more elegant one?

[Thinking] : I don’t know if my friend noticed that when I was carrying out Currization, I switched the position of the parameters of the function, because the order of currization is that the parameters are from left to right, so in order to achieve package discount, the discount parameters must be advanced. This is a simple scenario, encapsulating binary functions, if it is multivariate complex, and there are many applications, we can not guarantee to encapsulate the first, then what to do? I’ll leave that for reflection, and I’ll come back to that.

Case 2: Encapsulating logging functions

This is a very simple scenario, and you probably don’t need it, but it’s a good example to start with. The following is a slightly more complicated example that I think is more appropriate to help us understand the benefits of Corrification.

For example, we have a logging function that prints error messages to the console as follows:

const loggerHelper = (type, errorMsg, lineNo) = > {
    if (type === 'DEBUG') {
        console.debug(errorMsg + ' at line: ' + lineNo);
    } else if (type === 'ERROR') {
        console.error(errorMsg + ' at line: ' + lineNo);
    } else if (type === 'WARN') {
        console.warn(errorMsg + ' at line: ' + lineNo);
    } else {
        throw "wrong type"}}Copy the code

In other words, we use one function to print different log types. To make code reading more intuitive, we can actually use the following encapsulation:

/ / packaging
const debugLogger = curry(loggerHelper)('DEBUG');
const errorLogger = curry(loggerHelper)('ERROR');
const warnLogger = curry(loggerHelper)('WARN');
/ / use
debugLogger('debug msg'.21);
errorLogger('error msg'.33);
warnLogger('warn msg'.89);
Copy the code

The above are two simple cases of the application of Coriolization. In fact, in the daily development process, Coriolization may not be often used, but in fact, if it is used, It can indeed bring convenience to development and subsequent maintenance. I hope you can remember to use ~ when you meet the right scene after reading it

Partial function

Ok, so we finally get to partial functions which is the problem I mentioned in the beginning of the reading summary, so I won’t continue with that problem here. Let’s first look at the above thinking: when carrying out the function of membership system Coriolization, we specially changed the transfer order of function parameters, because to encapsulate the fixed discount, so the fixed discount originally in the second parameter was mentioned in the first parameter, and then carried out the Coriolization encapsulation. So can we not change the parameter order of the original function, and still continue to wrap? The answer is the partial function.

[Important] : So, as mentioned at the beginning, both Currie and partial functions are important in the concept of functional programming, but we only need to use one of them in certain scenarios.Copy the code

Partial function definition

Although we have compared partial functions with Currization, they are not completely opposite. Currization is the currization of parameters from left to right, while partial functions are not encapsulated from right to left. Partial functions are functions that can be partially manipulated by the developer on the function’s fixed arguments.

Partial functions focus on partial parameters and do not emphasize the order of functions.

Partial function implementation

Let’s ignore the partial function mentioned above, or start from scratch step by step implementation, slowly find the problem to solve the problem, more help to understand the partial function.

Implementation case: encapsulate a universal version of 1s delay function

As we all know, setTimeout(fn, delay) is a built-in function of JS, which means that it can’t change the order like the membership system above, so we can’t use corrification to package (of course not absolutely not, but it will take a step). So let’s say we have a system that calls a lot of this 1s delay function. So it’s more convenient to encapsulate it in advance.

  • The normal application
/ / application
const timer1 = setTimeout(() = > console.log('task 1'), 1000);
const timer2 = setTimeout(() = > console.log('task 2'), 1000);
Copy the code
  • Ordinary packaging
// Common encapsulation
const setTimeout1s = fn= > setTimeout(fn, 1000);

/ / application
const time1 = setTimout1s(() = > console.log('task1'))
const time2 = setTimout1s(() = > console.log('task2'))
Copy the code
  • Cremation package

Having said that, it’s not that you can’t encapsulate it with Corrification, it’s just that you don’t need it, and here’s why you don’t:

// Curried encapsulation
const newSetTimeout = (delay, fn) = > setTimeout(fn, delay);
const setTimeout1s = curry(newSetTimeout)(1000);

/ / application
const time1 = setTimout1s(() = > console.log('task1'))
const time2 = setTimout1s(() = > console.log('task2'))
Copy the code
  • Partial function encapsulation
// Partial function encapsulation
const setTimeout1s = patial(setTimeout.undefined.1000);

/ / application
const time1 = setTimout1s(() = > console.log('task1'))
const time2 = setTimout1s(() = > console.log('task2'))
Copy the code

There are three types of encapsulation, as can be seen, the first is forced encapsulation of the scene is not universal, do not consider; In the second case, an extra function is needed to encapsulate the conversion parameters before corridian encapsulation, which is more troublesome and is not considered; The third, partial function encapsulation, which we haven’t implemented yet, is obviously more suitable for this operation when we do.

Implementation 1: Simple buggy version

Const setTimeout1s = patial(setTimeout, undefined, 1000); setTimeout1s = patial(setTimeout, undefined, 1000); Patial takes three arguments. The first argument is the wrapped function itself, and the next argument is the number and order of arguments received by the original function. But because we are specifying arguments in the wrapped part, undefined is used for the unfixed part to be passed in.

// Implement partial function for the first time
const partial = (fn, ... partialArgs) = > {
  // Assign the partial function to args except for the first argument fn
  let args = partialArgs;
  // Returns a function that also takes a list of arguments
  return (. fullArgs) = > {
    let arg = 0;
    / / traverse two parameter list, respectively in the external packaging function parameter list and internal calls the function parameter list, external packaging function argument list is undefined, the significance of undefined said, encounter the undefined parameters be replaced, Undefined is a placeholder that encapsulates a fixed argument, and the replacement is the argument of the internal call.
    for (let i = 0; i < args.length && arg < fullArgs.length; i++) {
      if (args[i] === undefined) { args[i] = fullArgs[arg++]; }}return fn.apply(null, args); }}Copy the code

The code is not much to explain, I have written the comments are very clear. Or directly apply to know:

  • Case One – EncapsulationparseInt

As we all know, parseInt takes two arguments, the first is the string to be converted and the second is the conversion coefficient. Generally we choose the default decimal. (The second argument must be passed because it can be buggy in some scenarios if not passed), so you can encapsulate a default decimal safe parseInt.

window.tenParseInt = partial(parseInt.undefined.10);
/ / or
window.tenParseInt = function (str) {
  return partial(parseInt.undefined.10)(str);
}

/ / application
tenParseInt('123'); / / 123
Copy the code

[Explanation] : Args = [undefined, 10], fullArgs = [‘123’], fullArgs = [‘123’], fullArgs = [‘123’] (args[0] = fullArgs[0] = ‘123’) (args[0] = fullArgs[0] = ‘123’) Apply (null, args) = fn(‘123’, 10) = parseInt(‘123’, 10).

  • Case 2 – EncapsulationJSON.stringfy

Json.stringify (STR, replacer, TAB) can take three arguments: the first is the string to convert, the second can be a function or array, and the third can be converted to add a format (space or newline).

const tab4Stringify = partial(JSON.stringify, undefined.null.4);
const obj = { name: 'luffyzh'.email: '[email protected]' };
tab4Stringify(obj);
{
    "name": "luffyzh"."email": "[email protected]"
}
Copy the code

We wrapped the above function, and it looked fine, but now we have the function that we mentioned at the beginning, in fact, when called, this function is broken.

const obj2 = { name: 'naruto'.email: '[email protected]' };
tab4Stringify(obj2);
{
    "name": "luffyzh"."email": "[email protected]"
}
Copy the code

We see that the first call was successful, and the second call failed? Why is that? To recap: ** We use an array inside a function to store the contents of the argument list, so in JS, arrays are address references, which should be clear to us. ** After the first execution, our argument list args actually becomes [‘[object object]’, null, 4], which means that the first time we pass the parameter to undefined, but the second time we execute it, Undefined has been overwritten by the result of the first execution. Args has no undefined to replace. This is the nature of the problem.

Implementation two: Improve the Bug version

If we find a problem, we have to solve the problem, and the essence is that we should reset the parameter at the end of each execution, changing the position that should be replaced to undefined.

// For the first time
const partial = (fn, ... partialArgs) = > {
  // Assign the partial function to args except for the first argument fn
  let args = partialArgs;
  // Returns a function that also takes a list of arguments
  return (. fullArgs) = > {
    // Rework the placeholder, since the placeholder position is the first, reset args[0]
    args[0] = undefined;
    let arg = 0;
    If undefined is encountered in the outer function argument list, the inner function argument list is assigned to the outer function argument list one by one. Undefined means that the parameters of the inner function are also assigned to the arGS argument list from undefined
    for (let i = 0; i < args.length && arg < fullArgs.length; i++) {
      if (args[i] === undefined) { args[i] = fullArgs[arg++]; }}return fn.apply(null, args); }}const tab4Stringify = partial(JSON.stringify, undefined.null.4);
const obj = { name: 'luffyzh'.email: '[email protected]' };
// First call
tab4Stringify(obj);
{
    "name": "luffyzh"."email": "[email protected]"
}
const obj2 = { name: 'naruto'.email: '[email protected]' };
// Second call
tab4Stringify(obj2);
{
    "name": "naruto"."email": "[email protected]"
}
Copy the code

The improved version is complete, it looks like there is no problem, but in fact the improved version is also a big problem, why? The partial function is used to handle partial arguments. The partial function is used to handle partial arguments. Take this scenario:

  • Case 4: Encapsulate two partial parameters

We have a function that adds four functions — add4(a, b, c, d), and now we want to temporarily fix the first and third arguments to be 10 and 30, respectively. The second and the fourth are passed in, and obviously, you can’t use the Cremation and the encapsulation above, so you can use it if you don’t believe me. But the idea is pretty much the same, just write down the placeholder position of the external parameter list and reset it after execution.

Implementation 3: The final complete version

const partial = (fn, ... particalArgs) = > {
  // Convert to another array
  let args = particalArgs;
  // Record the position of the placeholder
  const keyPositions = args.reduce((acc, cur, index) = > {
    cur === undefined ? acc.push(index) : null;
    returnacc; } []);// Returns a function that also takes a list of arguments
  return (. fullArgs) = > {
    let arg = 0;
    // Reset the placeholder to undefined before each call
    for (let i = 0; i < keyPositions.length; i++) {
      args[keyPositions[i]] = undefined;
    }
    If undefined is encountered in the outer function argument list, the inner function argument list is assigned to the outer function argument list one by one. Undefined means that the parameters of the inner function are also assigned to the arGS argument list from undefined
    for (let i = 0; i < args.length && arg < fullArgs.length; i++) {
      if (args[i] === undefined) { args[i] = fullArgs[arg++]; }}return fn.apply(null, args); }}Copy the code

The first and third parameters are fixed, and the two parameters we passed are placed on the second and fourth parameters:

const add4 = (a, b, c, d) = > {
  return a + b + c + d;
}

const _add4 = partial(add4, 10.undefined.30.undefined);
_add4(20.40);
Copy the code

Implementation summary

Although it is realized, it is not the optimal solution. It is just to clarify the problem and principle. As for the places that you want to use, I still recommend you to use loDash package. Of course, we could have used a specific placeholder instead of undefined, which would have been more elegant.

conclusion

Currie and partial functions are an important part of functional programming, and their application is also a commonly used concept in JS – closure to achieve. Both of these are very useful in our daily development process, making the code intuitive, elegant and easy to maintain. They are also very important for understanding functional programming. I hope this article will make it easy for you to grasp them. Inadequacies are more considerate ~

The code address