简体   繁体   中英

Functional Programming - .bind.apply for curry function

Reading about functional programming - got to currying, example has a simple currying function. I understand everything except the last else block.

var curry = function (fn, fnLength) {
    fnLength = fnLength || fn.length;
    return function () {
        var suppliedArgs = Array.prototype.slice.call(arguments);
        if (suppliedArgs.length >= fn.length) {
            return fn.apply(this, suppliedArgs);
        } else if (!suppliedArgs.length) {
            return fn;
        } else {
            return curry(fn.bind.apply(fn, [this].concat(suppliedArgs)), fnLength - suppliedArgs.length);
       }
    };
};

If the supplied args are >= , call the function with the supplied arguments.

Else if suppliedArgs.length is falsy, return the original function without doing anything.

Else ???

  • I think recursively call the function?
  • I don't understand what .bind.apply accomplishes.
  • Is [this] just in an array because suppliedArgs.push wouldn't return the array?

Start by looking at how you call Function#bind() :

fun.bind(thisArg[, arg1[, arg2[, ...]]])

Then consider how you use Function#apply() :

fun.apply(thisArg, [argsArray])

So given for bind() we need to call it on a function, and give it multiple parameters (not an array), and all we have is an array of arguments ( suppliedArgs in the code), then how can we do that? Well, the main way you can call a function that takes multiple arguments instead of a single argument that is an array is to use .apply() on the function. So then we have fn.bind.apply(...something...) .

The first parameter to .apply() is the this value - which needs to be the function to be bound in the case of .bind() (see below for an explanation of why). Hence fn.bind.apply(fn, ...) .

Then, the second parameter to .apply() is an array of all the arguments to the function you are invoking, which in the case of .bind() is thisArg[, arg1[, arg2[, ...]]] . Hence we need a single array, with the first value being the value for this within the function, followed by the other arguments. Which is what [this].concat(suppliedArgs) produces.

So the whole fn.apply.bind(fn, [this].concat(suppliedArgs)) thing produces a correctly bound function that will have the supplied arguments to the current function "prefilled", with the correct this context. This function that is produced is then passed as the fn parameter in a recursive call to curry() , which in turn will produce another function in the same way as the top level call will.

The overall effect is that whenever you call a function created by curry() , if you don't pass the expected number of parameters, you will get a new function which takes the remaining number of parameters, or you will evaluate the original function with the entire list of parameters passed in correctly.

eg

function addAllNums(a, b, c, d, e) {
    return a + b + c + d + e;
}

var curriedAddAll = curry(addAllNums, 5);
var f1 = curriedAddAll(1); // produces a function expecting 4 arguments
var f2 = f1(2, 3); // f2 is a function that expects 2 arguments
var f3 = f2(4); // f3 is a function that expects 1 argument
var f4 = f3(5); // f4 doesn't expect any arguments
var ans = f4(); // ans = 1 + 2 + 3 + 4 + 5 = 15.
// OR var ans = f3(5); => same result

Why the different thisArg values?

Probably the most confusing thing about this line of code is the two different values for thisArg in .bind() and .apply() .

For .apply() , the thisArg is what you want the value of this to be inside the function you are calling .apply() on. eg myFunction.apply(myObj, ['param1', 'param2']) is equivalent to myObj.myFunction('param1', 'param2') .

In this particular case, .bind() is executed on the fn function, so we want fn to be the this value for .bind() , so it knows what function it is creating a bound version of.

For .bind() , the thisArg is what the value of this will be inside the bound function that is returned.

In our case, we want to return a bound function that has the same this value as we currently have. In other words, we want to maintain the this value correctly within the new function, so it doesn't get lost as you create new functions which happens when you call a curried function with less arguments than it expects.

If we did not maintain the this value correctly, the following example wouldn't log the correct value of this . But by maintaining it, the correct value will be output.

var myObj = { 
    a: 1, 
    b: curry(function (a, b, c, d) { 
           console.log('this = ', this); 
           return a + b + c + d; 
       })
};
var c = myObj.b(1,1,1); // c is a function expecting 1 argument
c(1); // returns 4, and correctly logs "this = Object {a: 1, b: function}"
      // if "this" wasn't maintained, it would log the value of "this" as
      // the global window object.

The last else block is the main and most important part of the curry function, as it is the actual line that carries the logic for currying.

return curry(fn.bind.apply(fn, [this].concat(suppliedArgs)), fnLength - suppliedArgs.length);

This is what returns the new function that needs n-1 arguments from your previous function. Why? It's a combination of multiple things:

fn.bind.apply simply calls a function in the context of the function itself, while supplying the needed args (suppliedArgs). Note how the next parameter to curry is fnLength - suppliedArgs.length, which reduces the arguments needed to what was passed.

Let's explain it with the help of ES6. Things are going to become more obvious.

// Imagine we have the following code written in ES5
function fn(a, b, c) {
  console.log(a, b, c);
}

var arr = [1, 2, 3];
var funcWithBoundArguments = fn.bind.apply(fn, [null].concat(arr));

Let's convert ES5 to ES6 code

// ES6
function fn(a, b, c) { console.log(a, b, c) }

let arr = [1,2,3];

let funcWithBoundArguments = fn.bind(null, ...arr)

You see? When you bind a function we have to explicitly enumerate all the arguments like:

fn.bind(null, 1, 2, 3)

But how could we bind the content of an array if we don't know its content in advance?

Right, we have to use .bind.apply() where:

  • the 1st argument of apply is the function ( fn ) we bind
  • the 2nd argument is an array which gets the context (as the first item of array) that we bind our function to and the rest of the items of the array are the arguments (which number is variable) we bind to our function ( fn ).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM