Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Append arguments to Javascript functions, rather than prepend (Function.prototype.bind)

In Javascript (Node.js context), I use Function.prototype.bind regularly: bind allows for changing invocation context and optionally supplying additional prepended arguments.

Are there any recommendations for appending arguments? On a couple occasions I have encountered the need to append rather than prepend in Node.js so I can adhere to its function signature pattern.

Now for a semi-practical and simplified example; I'm using the async module's eachSeries method.

First, an implementation that wraps the callback (working, but long way):

function func(something,callback) {
    async.eachSeries(
        [1,2,3],
        function iterator(item,asyncCallback) {
            // do stuff
            asyncCallback(err||null);
        },
        function finished(err) {
            // `callback` expects 2 arguments
            // `err` should always be the first arg, null or otherwise
            // `something` (unrelated to the async series) should be maintained
            callback(err,something);
        }
    );
};

And now something a bit shorter:

function func(something,callback) {
    async.eachSeries(
        [1,2,3],
        function iterator(item,asyncCallback) {
            // do stuff
            asyncCallback(err||null);
        },
        callback.bindAppend(this,something)
        // pseudo-equiv: `callback.bind(this,err,something);`
    );
};

In the second example, the err is carried from eachSeries's asyncCallback callback, while something is provided by other means. To clarify, I'm looking to replace bindAppend from the "shorter" example that would have the functional equivalent of function finished in the "longer" example.

Perhaps my design is flawed and needs to be reworked or this is just another case of premature optimization. However, the feature I seek could provide the following benefits:

  1. simplified code in regards to legibility IMO
  2. decreased and simplified stack depth

One answer would be to roll my own from a forked Function.prototype.bind polyfill. However, I'm looking for either a native implementation that I'm not seeing, a close-to-native work-around, or a utility module that has the hard work already done (optimization, testing, etc.). FYI, any solution other than the former would actually worsen feature benefit #2.

like image 715
zamnuts Avatar asked Apr 30 '14 06:04

zamnuts


4 Answers

Assuming i understood the question,my first shot at it with tests :

Function.prototype.bindAppend = function(context) {
    var func = this;
    var args = [].slice.call(arguments).slice(1);
    return function() {
        return func.apply(context, [].slice.call(arguments).concat(args));
    }
}

Test:

Function.prototype.bindAppend = function(context) {
    var func = this;
    var args = [].slice.call(arguments).slice(1);
    return function() {
        return func.apply(context, [].slice.call(arguments).concat(args));
    }
}

describe('bindAppend', function() {
    beforeEach(function() {
        this.obj = {
            value: "a",
            func: function() {
                return this.value + [].slice.call(arguments).join('');
            }
        }
    });

    it('should work  properly', function() {
        expect(this.obj.func.bindAppend(this.obj, "c")("b")).toEqual("abc");
    });
    it('should work  properly', function() {
        expect(this.obj.func.bindAppend(this.obj, "c", "d")("b")).toEqual("abcd");
    });
    it('should work  properly', function() {
        expect(this.obj.func.bindAppend(this.obj, "d")("b", "c")).toEqual("abcd");
    });
})

Working version: https://mparaiso.github.io/playground/#/gist/hivsHLuAuV

like image 154
mpm Avatar answered Oct 16 '22 22:10

mpm


There are no native solutions. But I can propose you something like this:

Function.prototype.bindAppend = function (context) {
  var bindArgs = Array.prototype.slice.call(arguments, 1);
  var func = this;
  return function() {
    var funcArgs = Array.prototype.slice.call(arguments);
    var args = bindArgs.map(function(arg) {
      if (arg === undefined) return funcArgs.shift();
      return arg;
    }).concat(funcArgs);
    func.apply(context, args);
  }
}

And usage:

function func(something,callback) {
    async.eachSeries(
        [1,2,3],
        function iterator(item,asyncCallback) {
            // do stuff
            asyncCallback(err||null);
        },
        callback.bindAppend(this, undefined, something)
        // pseudo-equiv: `callback.bind(this,err,something);`
    );
};

Other example:

function a() {console.log.apply(console, arguments)};
var b = a.bindAppend(null, 1, undefined, 3, undefined, 5);
b(2,4,6,7); //1,2,3,4,5,6,7
like image 42
3y3 Avatar answered Oct 16 '22 23:10

3y3


For the sake of maintainability, I would not add a method to Function to achieve this. The other answers to the question here contain two implementation of Function.prototype.bindAppend that are incompatible with each other. So imagine creating your own bindAppend and then having to use your application with a body of code whose author decided to do the same thing but uses a implementation incompatible with yours.

Polyfills are fine insofar as they aim to fill in a missing function according to a specific standard. A polyfill for Function.prototype.bind, for instance, is not free to deviate from ECMA-262, 5th edition. If it does deviate, then it is arguably "buggy" and should be replaced with an implementation that does not deviate from the standard. When one implements methods that are not defined by a standard there's no such constraint.

The following benchmarking suite shows the difference between various ways of attaining the result in the question.

var async = require("async");
var Benchmark = require("benchmark");
var suite = new Benchmark.Suite();

function dump (err, something) {
    console.log(arguments);
    console.log(err, something);
}

function addToSuite(name, makeFinished) {
    function func(something, callback) {
        async.eachSeries([1,2,3],
                         function iterator(item, asyncCallback) {
                             var err;
                             // do stuff
                             asyncCallback(err || null);
                         },
                         makeFinished(this, something, callback)
                        );
    }

    console.log(name, "dump");
    func("foo", dump);
    console.log("");

    suite.add(name, function () {
        func("foo", function (err, something) {});
    });
}

// Taken from mpm's http://stackoverflow.com/a/23670553/1906307
Function.prototype.bindAppend = function(context) {
    var func = this;
    var args = [].slice.call(arguments).slice(1);
    return function() {
        return func.apply(context, [].slice.call(arguments).concat(args));
    };
};

addToSuite("anonymous function", function (context, something, callback) {
    return function finished(err) {
        callback(err, something);
    };
});

addToSuite("mpm's bindAppend", function (context, something, callback) {
    return callback.bindAppend(context, something);
});

addToSuite("addErr, only one parameter", function (context, something,
                                                   callback) {
    function addErr(f, something) {
        return function (err) {
            return f(err, something);
        };
    }

    return addErr(callback, something);
});

addToSuite("addErr, multi param", function (context, something, callback) {
    function addErr(f, one, two, three, four, five, six) {
        return function (err) {
            return f(err, one, two, three, four, five, six);
        };
    }

    return addErr(callback, something);
});

addToSuite("addErr, switch", function (context, something, callback) {
    function addErr(f, one, two, three, four, five, six) {
        var args = arguments;
        return function (err) {
            switch(args.length) {
            case 1: return f(err);
            case 2: return f(err, one);
            case 3: return f(err, one, two);
            case 4: return f(err, one, two, three);
            case 5: return f(err, one, two, three, four);
            case 6: return f(err, one, two, three, four, five);
            case 6: return f(err, one, two, three, four, five, six);
            default: throw Error("unsupported number of args");
            }
        };
    }
    return addErr(callback, something);
});

suite
    .on('cycle', function(event) {
        console.log(String(event.target));
    })
    .on('complete', function() {
        console.log('The fastest is ' + this.filter('fastest').pluck('name'));
    })
    .run();

Output:

anonymous function dump
{ '0': undefined, '1': 'foo' }
undefined 'foo'

mpm's bindAppend dump
{ '0': 'foo' }
foo undefined

addErr, only one parameter dump
{ '0': undefined, '1': 'foo' }
undefined 'foo'

addErr, multi param dump
{ '0': undefined,
  '1': 'foo',
  '2': undefined,
  '3': undefined,
  '4': undefined,
  '5': undefined,
  '6': undefined }
undefined 'foo'

addErr, switch dump
{ '0': undefined, '1': 'foo' }
undefined 'foo'

anonymous function x 4,137,843 ops/sec ±3.18% (93 runs sampled)
mpm's bindAppend x 663,044 ops/sec ±1.42% (97 runs sampled)
addErr, only one parameter x 3,944,633 ops/sec ±1.89% (91 runs sampled)
addErr, multi param x 3,209,292 ops/sec ±2.57% (84 runs sampled)
addErr, switch x 3,087,979 ops/sec ±2.00% (91 runs sampled)
The fastest is anonymous function

Notes:

  • The dump calls dump to screen what it is the final callback gets when there is no error whatsoever.

  • When there is no error, mpm's bindAppend implementation will call callback with only one parameter that has the value "foo". This is a very different behavior from the original function finished(err) { callback(err,something);} callback it is meant to replace.

  • Everything is faster than mpm's bindAppend.

  • I would not use the addErr, multi param implementation. I put it there to check how that approach would perform.

  • The addErr functions are faster than bindAppend, at the cost of flexibility. They add only one parameter to the callback.

At the end of the day, I'd most likely use the addErr, switch implementation with a large enough number of cases to handle the needs of my code. If I needed something which would have absolute flexibility, for some limited cases, I'd use something similar to bindAppend but not as a method on Function.

like image 28
Louis Avatar answered Oct 16 '22 23:10

Louis


lodash _.partial allows you to append arguments..

https://lodash.com/docs/4.17.4#partial

function test(a, b, c) {
    return a + ' ' + b + ' ' + c;
}

test.bind(null, 1)(2, 3); // 1 2 3

_.partial(test, _, _, 1)(2, 3); // 2 3 1
like image 29
shunryu111 Avatar answered Oct 16 '22 23:10

shunryu111