开发者

JavaScript curry

开发者 https://www.devze.com 2023-02-14 07:08 出处:网络
I`m a newbie at JavaScript trying to understand this tutorial about currying from Oreilly JavaScript Cookbook.

I`m a newbie at JavaScript trying to understand this tutorial about currying from Oreilly JavaScript Cookbook.

Could someone be kind enough to explain this program in detail step by step in plain language. Please make sure to explain the "nul开发者_运维问答l" argument passed in the second last line of the program. Thank you in advance if you can help.

function curry(fn, scope) {
    scope = scope || window;
    var args = [];
    for (var i = 2, len = arguments.length; i < len; ++i) {
        args.push(arguments[i]);
    }
    return function() {
        var args2 = [];
        for (var i = 0; i < arguments.length; i++) {
            args2.push(arguments[i]);
        }
        var argstotal = args.concat(args2);
        return fn.apply(scope, argstotal);
    };
}

function diffPoint(x1, y1, x2, y2) {
    return [Math.abs(x2 - x1), Math.abs(y2 - y1)];
}

var diffOrigin = curry(diffPoint, null, 3.0, 4.0);
var newPt = diffOrigin(6.42, 8.0); //produces array with 3


If you dont mind a suggestion, start with Javascript: The Good Parts. Follow that up with either Javascript Patterns, or Secrets of the Javascript Ninja for more advanced techniques. Cookbooks are more for canned solutions to problems then a learning resource.

Matt Ball did a good job explaining whats going on. If you are a beginner, I wouldn't sweat trying to figure out curry functions anyways. That aside, IMO this curry function is terrible. This is how I would change it

// this is doing binding and partial function application, 
// so I thought bind was a more appropriate name
// The goal is that when you execute the returned wrapped version of fn, its this will be scope
function bind(fn, scope) {
  // arguments is an implicit variable in every function that contains a full list
  // of what was passed in. It is important to note that javascript doesn't enforce arity.
  // since arguments is not a true array, we need to make it one.
  // a handy trick for this is to use the slice function from array,
  // since it will take arguments, and return a real array.
  // we are storing it in a variable, because we will need to use it again.
  var slice =  Array.prototype.slice,
      // use slice to get an array of all additional arguments after the first two
      // that have been passed to this function.
      args = slice.call(arguments, 2);

  // we are returning a function mostly as a way to delay the execution.
  // as an aside, that this is possible in a mainstream language is a minor miracle
  // and a big part of why i love javascript.
  return function() {
    // since functions are objects in javascript, they can actually have methods.
    // this is one of the built in ones, that lets you execute a function in a different
    // context, meaning that the this variable inside the 
    // function will actually refer to the first argument we pass in.

    // the second argument we are jamming together the arguments from the first function
    // with the arguments passed in to this wrapper function, and passing it on to fn.
    // this lets us partially apply some arguments to fn when we call bind.
    return fn.apply(scope, args.concat(slice.call(arguments)));
  }
}

JavaScript, while wonderful, is horribly verbose. Needlessly repeating var while defining your bindings just adds a lot of noise. Also, there is no need to painfully build a real array like that, slice will take arguments and give you a real array back. Especially in this case where we are using it twice, AND we actually want to slice out the first two args anyways. Finally, when you apply and your first arg is null, JavaScript will apply the global object for you. There is no need to do that explicitly.

IMO my 5 line function body kicks the crap out of o'reillys 11 lines, and IMO it is much more readable.


// define the curry() function
function curry(fn, scope) {

    // set the scope to window (the default global object) if no scope was passed in.
    scope = scope || window;

    // Convert arguments into a plain array, because it is sadly not one.
    // args will have all extra arguments in it, not including the first 2 (fn, scope)
    // The loop skips fn and scope by starting at the index 2 with i = 2
    var args = [];
    for (var i = 2, len = arguments.length; i < len; ++i) {
        args.push(arguments[i]);
    }

    // Create the new function to return
    return function() {

        // Convert any arguments passed to the this function into an array.
        // This time we want them all
        var args2 = [];
        for (var i = 0; i < arguments.length; i++) {
            args.push(arguments[i]);
        }

        // Here we combine any args originally passed to curry, with the args
        // passed directly to this function.
        //   curry(fn, scope, a, b)(c, d)
        // would set argstotal = [a, b, c, d]
        var argstotal = args.concat(args2);

        // execute the original function being curried in the context of "scope"
        // but with our combined array of arguments
        return fn.apply(scope, argstotal);
    };
}

// Create a function to be curried
function diffPoint(x1, y1, x2, y2) {
    return [Math.abs(x2 - x1), Math.abs(y2 - y1)];
}

// Create a curried version of the diffPoint() function
//   arg1: the function to curry
//   arg2: the scope (passing a falsy value causes the curry function to use window instead)
//   arg3: first argument of diffPoint() to bake in (x1)
//   arg4: second argument of diffPoint() to bake in (y1)
var diffOrigin = curry(diffPoint, null, 3.0, 4.0);

// Call the curried function
// Since the first 2 args where already filled in with the curry, we supply x2 and y2 only
var newPt = diffOrigin(6.42, 8.0);

In this case the scope argument isn't used at all. scope sets what the this object is. The function you are currying doesn't use this so it has no real effect. The scope is set when fn.apply(scope, args) is called, which both sets the scope to run in and provides arguments to pass in.


The function curry allows you to bind a function f (the first parameter to curry) to a scope c (the second parameter), with optional additional arguments (the rest of the parameters).

That means that this function call:

curry(func, scope);

returns a function newFunc whose invocation:

var newFunc = curry(func, scope); // get the new function
newFunc(); // now invoke it

is equivalent to this:

scope.func();

The net effect of all this is for the this keyword to refer to scope* inside of func.


Concrete example time

Let's say that scope is a simple JS object with one property:

var scope = {name: 'Inigo Montoya'};

and that f is a function which wants to use some value inside of scope:

function f() {
    return 'My name is ' + scope.name;
}

and call it, like this:

f(); // returns 'My name is Inigo Montoya'

Well, that's one way to do it. It works.

Another way to do it would be using the curry function. Instead of f having to know to reference the scope object, scope is now the function's invocation context. Now the function can use the this keyword!

function f_new() {
    return 'My name is ' + this.myName; // see the difference?
}

var sayIt = curry(f, scope);

Now sayIt is a function that doesn't care what scope is called. It's like sayIt is defined on the scope object, like this:

var scope = { name: 'Inigo Montoya',
              sayIt: f_new }

...except that's not actually how scope is defined. sayIt just works that way. Now, we can call sayIt, like this:

sayIt(); // returns 'My name is Inigo Montoya'

Still with me?

Phew. The point of all this is to say that, in your example, null is being provided as the scope for diffOrigin to run it, because it doesn't care about what's in the scope. The line (in curry) scope = scope || window; means that, if scope is a falsy value (which null is) then the new function (diffOrigin in this case) will execute in global scope: this will refer to window.


Does that make sense to you?


*which is called the "invocation context"


Squeegy posted a good breakdown, but I figured I'd add mine too.

//Things to note, 'arguments' is a special variable in javascript that holds 
//an array like object full of all the things passed into a function.
//You can test this out with a function like this:
//var alertArgs = function(){alert(arguments);};

function curry(fn, scope) {
    //Either use the passed in 'scope' object, or the window object as your scope
    scope = scope || window;
    //Create a new array for storing the arguments passed into this function
    var args = [];
    //Loop through the extra arguments (we start at '2' because the first two
    //arguments were stored in `fn` and `scope` respectively.
    //We store these in the temporary 'args' array.
    //(in the example, args will end up looking like: [3.0, 4.0])
    for (var i = 2, len = arguments.length; i < len; ++i) {
        args.push(arguments[i]);
    }
    //We return the 'curried' function
    return function() {
        //This array is not used. I assume it is an error.
        var args2 = [];
        //We now have a new set of arguments, passed in to the curried function
        //We loop through these new arguments, (in the example, 6.42 and 8.0)
        //and add them to the arguments we have already saved. In the end, we have
        //the args array looking like: [3.0, 4.0, 6.42, 8.0]
        for (var i = 0; i < arguments.length; i++) {
            args.push(arguments[i]);
        }
        //This line isn't needed, because args2 is always blank.
        var argstotal = args.concat(args2);

        //Finally we call the function, passing in the full array of arguments
        return fn.apply(scope, argstotal);
    };
}

//This function takes 4 arguments
function diffPoint(x1, y1, x2, y2) {
    return [Math.abs(x2 - x1), Math.abs(y2 - y1)];
}

//We partially apply the first 2 arguments, so x1 is always 3.0, 
//and y1 is always 4.0
var diffOrigin = curry(diffPoint, null, 3.0, 4.0);

//We can now call 'diffPoint' indirectly, without having to specify 
//3.0, 4.0 as the first 2 arguments.
var newPt = diffOrigin(6.42, 8.0); //produces array with 3


This version allows for partial application which returns a new curried function.

function curry(fn) {
  const args = [];
  let i = 0;
  const n = arguments.length;

  while (++i < n) {
    args.push(arguments[i]);
  }

  // Functions have a 'length' property which tells us their 'arity'
  // 'arity' means the number of arguments a function can take.
  // https://en.wikipedia.org/wiki/Arity
  //
  // Here we count the number of arguments that we have and compare
  // it to the number of arguments the function can take.
  // If the function takes an equal or more amount, we have all our
  // arguments and execute the function.
  //
  return args.length >= fn.length
    // '.apply' will convert an array to a list of arguments.
    // 'null' is the context, which is essentially 'this'
    ? fn.apply(null, args) 
    : function () {
      let i = -1;
      const n = arguments.length;
      const args2 = [];

      while (++i < n) {
        args2.push(arguments[i]);
      }

      // We return a curried function which will get checked
      // for arity. Using recursion, we can keep creating newly
      // partially applied functions which can help us to 'compose'
      // new functions.
      // https://en.wikipedia.org/wiki/Function_composition_%28computer_science%29
      return curry.apply(null, [fn].concat(args.concat(args2)));
    };
}
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号