Bug Tracker

Opened 8 years ago

Closed 8 years ago

Last modified 7 years ago

#10811 closed bug (invalid)

$.when provides incoherent arguments to .done() when called with multiple values

Reported by: xmorel Owned by:
Priority: low Milestone: None
Component: deferred Version: 1.7
Keywords: Cc:
Blocked by: Blocking:

Description

A single argument $.when forwards anything it got directly to its resolution (if the argument is a non-deferred it provides the non-deferred, if it's a deferred all values resolved by the deferred are resolved by the when), but it has a problematic inconsistency when used with multiple arguments:

  • deferred resolving multiple values (e.g. jqxhr) result in $.when providing an array of these arguments to .done()
  • non-deferred or deferred resolving a single value result in $.when providing the value as-is to its .done(), unwrapped
  • Deferred resolving to no value at all result in $.when providing undefined to its .done() (at least it's there)

These changing behaviors make it extremely difficult to use $.when to correctly compose arbitrary deferreds (and values), especially if the deferred varies in the number of arguments it resolves, or if it can resolve arrays, or if the API (and thus the number of values resolved by the deferred) is still changing.

I think this should be changed in the following manner:

  • $.when(arg) stays unchanged, it's fine
  • $.when(arg+) should result in every argument to $.when mapping to an array in the .done() callback:
    • A deferred resolving multiple values stays as-is (array of all parameters)
    • A value or a deferred resolving a single value yields an array of a single value
    • A deferred resolving no value yields an empty array

This has two major advantages:

  • Values resolved by $.when can be consistently sent to other functions (an other $.when or a .resolve() or .resolveWith() call), without wondering if this precise one need to be called via .call() or via .apply()
  • changing number of values resolved by a deferred has a much lower influence on $.when's result: if the .done() used the first value resolved it still does, whether the deferred resolves 1 or 17 values (of course if the deferred goes from 1 to 0 values resolved the code will break, but that's sensible I think)

You can observe this behavior at http://jsfiddle.net/Xytms/1/:

$.when(novalue, onevalue).then(function () {
    console.log(arguments);
});

yields [undefined, 1], under this scheme it'd yield [[], [1]]

$.when(onevalue, twovalues, threevalues).then(function () {
    console.log(arguments);
});

yields [1, [1, 2], [1, 2, 3]], under this scheme it'd yield [[1], [1, 2], [1, 2, 3]]

$.when(null, 42, novalue, onevalue, twovalues).then(function () {
    console.log(arguments);
});

yields [null, 42, undefined, 1, [1, 2]], under this scheme it'd yield [[null], [42], [], [1], [1, 2]]

Change History (6)

comment:1 Changed 8 years ago by jaubourg

Component: unfileddeferred
Priority: undecidedlow
Resolution: invalid
Status: newclosed

It seems to me you expect your Deferreds to always be resolved as an array so that you can use said value in an apply (I think we can agree it's quite a narrow use-case). It's easy enough to make sure the Promises provided to $.when always exhibit the behaviour you expect using pipe: http://jsfiddle.net/2ktvR/

Or better yet, have your Deferred resolve with a single array rather than multiple arguments and your problem will automagically disapear.

Current behaviour of $.when is there to make as much sense as possible in simple (and most common) use cases. That's why it will try and not wrap a single value into an array to make said value easier to access.

comment:2 Changed 8 years ago by xmorel

It seems to me you expect your Deferreds to always be resolved as an array

No. All deferreds are resolved with parameters (0..n), and these parameters are an array. That's just a fact of life, that's pretty much what the arguments object is.

What I *would* expect is that $.when treats these arrays consistently, which it does not do (I outlined the cases). This has nothing to do with deferreds themselves, it has to do with $.when.

It's easy enough to make sure the Promises provided to $.when always exhibit the behaviour you expect using pipe: http://jsfiddle.net/2ktvR/

This, however, require significant additional verbosity to make $.when simply coherent.

Or better yet, have your Deferred resolve with a single array rather than multiple arguments and your problem will automagically disapear.

I don't have control of all the deferreds I use. This simply is not an option.

Furthermore, it makes using the deferred raws much more complex, as one incurs a systematic indirection for little reason.

Current behaviour of $.when is there to make as much sense as possible in simple (and most common) use cases.

And that's the issue I have with it: the behavior makes sense when wrapping a single deferred or value ($.when acts as a simple proxy), but the incoherences are problematic when using $.when to multiplex deferreds, as each parameter of $.when can pretty arbitrarily vary between undefined, a single value (which may be an array) and an array of values with no indication of which is which.

comment:3 in reply to:  2 Changed 8 years ago by jaubourg

Replying to xmorel:

It's easy enough to make sure the Promises provided to $.when always exhibit the behaviour you expect using pipe: http://jsfiddle.net/2ktvR/

This, however, require significant additional verbosity to make $.when simply coherent.

Here: http://jsfiddle.net/QA3xC/ verbosity gone (apart from the new when name of course).

Or better yet, have your Deferred resolve with a single array rather than multiple arguments and your problem will automagically disapear.

I don't have control of all the deferreds I use. This simply is not an option.

Furthermore, it makes using the deferred raws much more complex, as one incurs a systematic indirection for little reason.

Little reason being: make the most *common* use-case as easy and natural as possible. You are *not* the center of the universe, the millions of users joining two promises using $.when are.

Current behaviour of $.when is there to make as much sense as possible in simple (and most common) use cases.

And that's the issue I have with it: the behavior makes sense when wrapping a single deferred or value ($.when acts as a simple proxy), but the incoherences are problematic when using $.when to multiplex deferreds, as each parameter of $.when can pretty arbitrarily vary between undefined, a single value (which may be an array) and an array of values with no indication of which is which.

No, the behaviour makes sense when joining Deferreds too by not promoting a single resolve value that is not an array into an array just because it's "more consistent" (which it is not, it is just more convenient for your specific use-case).

Here is for consistency using your technique:

    $.when( $.when( $.when( 6 ), $.when( "hello" ) ) ).done( function( a ) {
        // Do you expect a to be [ 6, "hello" ]
        // Or [ [ [ 6 ], [ "hello" ] ] ]?
        // Which one makes more sense and is easier to browse?
    });

If you think this is contrived, just imagine if the code providing you the Promise constructed it using $.when. You just replaced your 'not always an array problem' into 'an array of I-dunno-what-depth problem'.

Always wrapping into an array is simply not an option: it's no more consistent and exhibits far worse behaviours in complex scenarios.

Also, you have $.isArray to help you figure out what's going on and, yes, differentiating between a single resolve value that is an array and a list of arguments has been sacrificed here (again: keep things simple), but look at what $.when($.when(defer)) would yield with your solution and you'll see you're unable to make the distinction yourself.

If you're at a point where you need a specific format for the resolve values, maybe you'd better put on the social clothes and go and talk with the ones providing the Promises.

comment:4 Changed 8 years ago by xmorel

Here: http://jsfiddle.net/QA3xC/ verbosity gone (apart from the new when name of course).

At this point, it would likely be simpler to just copy/paste $.when's code and remove the special case of unwrapping single values in multiple-argument when calls. It would also avoid the unwarranted snark.

Little reason being: make the most *common* use-case as easy and natural as possible. You are *not* the center of the universe, the millions of users joining two promises using $.when are.

I wouldn't expect those promises to usually be both 1-argument promise. Hence it would not be the common case. Would you care to provide support for your claims, or do you just claim to be The Voice Of Everybody?

No, the behaviour makes sense when joining Deferreds too by not promoting a single resolve value that is not an array into an array just because it's "more consistent", which it is not

Of course it is, deferred results are arrays, $.when with 2+ arguments currently unwraps some results, replaces others by a semi-arbitrary result and leaves a third-category alone. How can you argue this is more consistent than leaving the result of the deferreds alone in all cases?

Here is for consistency using your technique:

I see you have not actually read what I wrote

If you think this is contrived

It's not contrived, it's a misrepresentation. Which is worse.

just imagine if the code providing you the Promise constructed it using $.when. You just replaced your 'not always an array problem' into 'an array of I-dunno-what-depth problem'.

Again, you either have not bothered reading what I wrote and latched on a few parts you understood or you are wishfully mis-representing what I'm suggesting.

Always wrapping into an array

The reality is that what I'm suggesting actually aliases to *not unwrapping* values. There is a special case in $.when's handling of multiple arguments, removing it yields my suggestion.

is simply not an option: it's no more consistent

As previously noted, this declaration is obvious nonsense.

and exhibits far worse behaviours in complex scenarios.

The only scenario in which it exhibits "far worse" behavior is the multiplexing of a number of one-argument deferred and only one-argument deferreds. Which is a pretty simple scenario, and definitely not one I'd expect to represent the majority of cases: most deferrer multiplexings would deal with synchronizing xhr requests (together or with an other event), which is *not* helped by the current behavior since jqxhr returns three arguments.

In fact, at best the current behavior does not help and at worst it increases the cognitive load of the developer by forcing him to wonder, for every single deferred involved, if the deferred has one or more than one argument.

Also, you have $.isArray to help you figure out what's going on and, yes, differentiating between a single resolve value that is an array and a list of arguments has been sacrificed here (again: keep things simple), but look at what $.when($.when(defer)) would yield with your solution and you'll see you're unable to make the distinction yourself.

You are sadly wrong, and prove once more you have not read what I wrote. All along, my proposal has been to change the behavior of a multi-arguments $.when call. This is specified in the first paragraph of the report (and the $.when code already does the distinction and handles the two cases very differently).

If you're at a point where you need a specific format for the resolve values, maybe you'd better put on the social clothes and go and talk with the ones providing the Promises.

And I'd expect them to reply they're very sorry but they're not going to make up crazy alternative protocols due to the inconsistencies of the jQuery API. And they'd be right. They're not building their deferreds specifically for multiplexing via $.when.

Last edited 8 years ago by xmorel (previous) (diff)

comment:5 in reply to:  4 Changed 8 years ago by jaubourg

function getInfos() {
    var promises = [];
    $.each( Array.prototype.slice.call( arguments, 0 ), function( _, id ) {
        promises.push( $.ajax( "myData?id=" + id ).pipe(function( data ) {
            return data;
        }) );
    });
    return $.when.apply( $, promises );
}

function getTemplates() {
    var promises = [];
    $.each( Array.prototype.slice.call( arguments, 0 ), function( _, id ) {
        promises.push( $.ajax( "myTemplate?id=" + id ).pipe(function( data ) {
            return data;
        }) );
    });
    return $.when.apply( $, promises );
}

$.when( getInfos( 1, 2 ), getTemplates( "view", "detail" ) ).done(function( a, b ) {
    // Current behavior:
    // [ [ data1, data2 ], [ templateView, templateDetail ] ]
    // Your solution:
    // [ [ [ data1 ], [ data2 ] ], [ [ templateView ], [ templateDetail ] ] ]
});

// Added "noise"/complexity with your solution:

function deWrap() {
    var args = Array.prototype.slice.call( arguments, 0 );
    if ( args.length ) {
        $.each( args, function( i, arg ) {
            args[ i ] = arg[ 0 ];
        });
    }
    return args;
}

$.when(
    getInfos( 1, 2 ).pipe( deWrap ),
    getTemplates( "view", "detail" ).pipe( deWrap )
).done(function( a, b ) {
    // That's provided getInfos and getTemplates
    // always return a promise
});

Now, I use pipe after every ajax call because I don't want the code using my services to even know there's an ajax call involved. That way, I can easily code a fake/proxied method for testing purpose (or go get data from local storage, etc).

Your approach makes using pipe a nightmare, now I still have to pipe after a call to $.when just to make sure nothing's autobadly wrapped into an array.

So AGAIN, it's not more consistent, it's just a gigantic pain in the ass with piping acrobatics spreading all across the application, all that because YOU find it inconvenient to use $.isArray.

(and I had hoped you would have been able to generalize from my one element examples since the fact your expected behaviour is different for a single arg makes your proposal so much worse I didn't even want to get into it)

Now, feel free to discuss the example above at nauseum provided:

  1. you don't jump on implementation details as proof it is not a valid scenario because this or this or that: we both know we'll have contrived examples in posts in a bug tracker, bad faith is only good when it stops: the issue here is to know what happens when the promises you give to $.when are the output of previous calls to $.when (can we keep this civilized?)
  2. you stop paraphrasing "you don't get it" or "you don't read" as if they were arguments. I challenge you to provide code samples establishing your solution involves less work on the part of developpers in arbitrary situations where services can and will use $.when internally (maybe it's even the case in your specific use-case, have you checked with your co-workers?)

Thank you.

Last edited 8 years ago by jaubourg (previous) (diff)
Note: See TracTickets for help on using tickets.