caolan / highland

High-level streams library for Node.js and the browser
https://caolan.github.io/highland
Apache License 2.0
3.43k stars 147 forks source link

wrapCallback is uncurryable #296

Open brian-gates opened 9 years ago

brian-gates commented 9 years ago
var foo = _.wrapCallback(function (thing, done){
  done(null, thing);
});

foo('bar').apply(_.log); // bar

_.curry(foo)('bar').apply(_.log);
//          ^
// TypeError: object is not a function

The issue is that wrapCallback handles its arguments dynamically. I'm not sure there's any way around it, but I wanted to raise it for discussion as it would be a useful thing to be able to do. I suppose _.partial could be used instead, but that's less elegant.

// _.wrapCallback
function () {
    var self = this;
    var args = slice.call(arguments);
    return _(function (push) {
        var cb = function (err, x) {
            if (err) {
                push(err);
            }
            else {
                push(null, x);
            }
            push(null, nil);
        };
        f.apply(self, args.concat([cb]));
    });
}
apaleslimghost commented 9 years ago

There's not really a solution for this. The only way of creating a function of a given arity at runtime is the Function constructor, which would have a huge performance impact.

A workaround would be to use _.ncurry with the expected length of the function:

var foo = _.wrapCallback(function (thing, done){
  done(null, thing);
});

foo('bar').apply(_.log); // bar

_.ncurry(2, foo)('bar').apply(_.log);

I suppose we could have wrapCallback save the original function length as a separate property (length isn't writable) that curry could then use, but that only solves this particular case.

brian-gates commented 9 years ago

What if we curried by default? Seems within the spirit of this lib.

var wrapCallback = function (f) {
    return _.ncurry(f.length-1, function () {
        var self = this;
        var args = slice.call(arguments);
        return _(function (push) {
            var cb = function (err, x) {
                if (err) {
                    push(err);
                }
                else {
                    push(null, x);
                }
                push(null, nil);
            };
            f.apply(self, args.concat([cb]));
        });
    });
};

var foo = wrapCallback(function (thing, thing2, done){
  done(null, [thing, thing2]);
});

foo('bar')('baz').apply(_.log); // [ 'bar', 'baz' ]
vqvu commented 9 years ago

We can't, because the function being wrapped might have varargs. It's not in our control.

The only reasonable solution besides using ncurry is to have ncurry embed the length information in curried functions, which wrapCallback can use. So you can do something like this

var foo = _.wrapCallback(_.curry(function (thing1, thing2, done) {
    done(null, [thing1, thing2]);
}));

foo('bar')('baz').apply(_.log);

This essentially saves you from having to figure out the length of wrapped functions that aren't varargs (lets you use curry instead of ncurry). But it'll only work on highland-curried functions. The semantics would be: curried function -> curried function out.

brian-gates commented 9 years ago

Good point about varargs.

Curried in -> curried out seems sensible enough to me.