1

So i am having a bit of Dilemma, trying to refactor a set of nested anonymous closures in Javascript. However i am unsure the standard way of maintaining reference to parameters that otherwise would be seen by child closures.

for example

var foo;
var obj = new Obj();
obj.foobar(function (p1, p2, p3) {
    foo.onEvent(function (s1, s2) {
         if (s1 === 'bar') {
              s2.status = p2; //p2 has a reference here
         }
    });  
});

However if i refactor this code, so each closure it on its own named function, What would be the ideal way of getting a reference to p2?

example

var onFooEvent = function (s1, s2) {
             if (s1 === 'bar') {
                  s2.status = p2; //p2 will no longer has a reference here
             }
        }; 

var onFoobar = function (p1, p2, p3) {
        foo.onEvent(onFooEvent); 
    };

var foo;
var obj = new Obj();
obj.foobar(onFoobar);

MORE ROBUST EXAMPLE

var pooledQuery = function (query, params, env, res) {
    return new imports.promise(function(resolve, reject) {
        pool.acquire(function (err, client) {
            var db;
            if (err != null) {
                console.log(err)
                reject(err);
            }
            var meta = { env: env, res: res, result: [] };                

            db = client.query(query, params);
            db.on('result', function (res) {
                return res.on('row', function (row) {
                    return meta.result.push(row);
                }).on('error', function (err) {
                    meta.result = err;
                    reject(meta);
                }).on('end', function (info) {
                    return meta.result;
                });
            }).on('end', function () {
                pool.release(client);
                resolve(meta);
            });
        });
    });
};
DevZer0
  • 13,433
  • 7
  • 27
  • 51
  • Either you set it to a global variable or you pass it as a parameter. – Jo E. Aug 27 '15 at 02:35
  • 1
    I don't think there's anything wrong with leaving them nested if they need to access closure variables. You can still name them. – Bergi Aug 27 '15 at 03:07
  • Where is that `foo` coming from? – Bergi Aug 27 '15 at 03:09
  • What promise library are you using? It looks like you could improve your promise patterns. There's nothing wrong with your closures. – Bergi Aug 27 '15 at 04:46
  • @Bergi i was told to refactor all closures to named functions :-| i am using bluebird for `Promise` – DevZer0 Aug 27 '15 at 06:35
  • i am trying to see if this https://hughfdjackson.com/javascript/why-curry-helps/ adds any value to solve this problem. – DevZer0 Aug 27 '15 at 06:41
  • @DevZer0: Just [name them](http://kangax.github.io/nfe/). You don't need to move them elsewhere. Regardless, if you feel you must do that have a look at [this answer](http://stackoverflow.com/a/28250687/1048572) where I detail the two stages of unnesting. – Bergi Aug 27 '15 at 14:40
  • @Bergi thanks for your input, i solved the problem using `bind` :) – DevZer0 Aug 28 '15 at 03:48

2 Answers2

1

pass a curried function into foo.onEvent (and additionally change the arguments that onFooEvent accepts) -- something like this:

var onFooEvent = function (pArgs, s1, s2) {
  if (s1 === 'bar') {
    s2.status = pArgs[1]; 
  }
}; 

var onFoobar = function (p1, p2, p3) {
  foo.onEvent(_.curry(onFooEvent)(arguments)); 
};

here's a contrived runnable example:

var _ = require('lodash')

var print_stuff = function (p_args, s1, s2) {
  console.log(p_args[0])
  console.log(p_args[1])
  console.log(p_args[2])
  console.log(s1)
  console.log(s2)
}

var do_theoretical_stuff = function (p1, p2, p3) {
  return _.curry(print_stuff)(arguments)
}

var do_actual_stuff = do_theoretical_stuff(1, 2, 3) 

do_actual_stuff('a', 'b')

which outputs to the console:

1
2
3
a
b

Update: really great point made below about bind. the difference between bind and _.curry is that with bind you must set a context (via the first argument) at the time you use it. bind is basically doing two things for you: 1) setting context (i.e., explicitly binding/defining what this will actually be inside the function now rather than when the function is invoked) and 2) doing what curry does. if you don't need or want the first feature, then i think a cleaner approach is to use curry. (if you don't normally use a library like lodash, it's easy to find a standalone curry function that can be added to one's local utils.) this SO question has more on the topic: Javascript 's bind vs curry?

user2524973
  • 1,087
  • 6
  • 14
  • if you are working with an external library where event callback functions already have set of pre-defined arguments, how can it be modified? i pasted a an actual example – DevZer0 Aug 27 '15 at 04:15
  • so if you need to pass in a callback function to `pool.acquire` and that callback's arguments are only `err` and `client`, then *you* would write a function elsewhere called, say, `foo` that takes 6 arguments, `query, params, env, res, err, client`, and then the callback function that you explicitly pass into `pool.acquire` would be a function that is a currying of `foo` w.r.t. to the first four arguments, like this: `pool.acquire(_.curry(foo)(query)(params)(env)(res));` – user2524973 Aug 27 '15 at 11:06
  • i was able to solve my issue using `bind()` i don't know what's the advantage of using `curry` – DevZer0 Aug 28 '15 at 03:47
  • i +1 your answer for the approach. However i prefer the `bind` method. If you write a bit of a detailed answer covering `bind` and explaning why `curry` maybe better than `bind` i will accept your answer. – DevZer0 Aug 28 '15 at 03:53
  • 1
    great point, i have added some notes comparing bind and curry – user2524973 Aug 30 '15 at 00:04
  • thanks. it was an interesting question for me to think about :) – user2524973 Sep 09 '15 at 11:42
1

here is how i solved in using bind, there is bit more refactoring here where i plan to encapsulate all the functions inside another class instance. (However ignore that part) Just wanted to share how i solved it.

var DbPool = function () {

};

DbPool.prototype.onRow = function (meta, row) {
    return meta.result.push(row);
};

var onRowError = function (meta, reject, err) {
    meta.result = err;
    reject(meta);
};

var onRowEnd = function (meta, info) {
    return meta.result;
};

var onResult = function (meta, reject, res) {

    var onRowBind = DbPool.prototype.onRow.bind(this, meta);
    var onRowErrorBind = onRowError.bind(this, meta, reject);
    var onRowEndBind = onRowEnd.bind(this, meta);

    return res.on('row', onRowBind).on('error', onRowErrorBind).on('end', onRowEndBind);
};

var onEnd = function (meta, resolve, client) {
    pool.release(client);
    resolve(meta);
};

var dbPoolAcquired = function (query, params, env, res, resolve, reject, err, client) {
    var db;
    if (err != null) {
        console.log(err)
        reject(err);
    }
    var meta = { env: env, res: res, result: [] };

    logger.debug("Query %s Params %s", query, params);

    db = client.query(query, params);

    var onResultBind = onResult.bind(this, meta, reject);
    var onEndBind = onEnd.bind(this, meta, resolve, client);

    db.on('result', onResultBind).on('end', onEndBind);
};

var dbPoolPromised = function(query, params, env, res, resolve, reject) {
    var dbPoolAcquiredBind = dbPoolAcquired.bind(undefined, query, params, env, res, resolve, reject)
    pool.acquire(dbPoolAcquiredBind);
};

var pooledQuery = function (query, params, env, res) {
    var dbPoolPromisedBind = dbPoolPromised.bind(this, query, params, env, res);
    return new imports.promise(dbPoolPromisedBind);
};
DevZer0
  • 13,433
  • 7
  • 27
  • 51