Promises from A to J

Source: Internet
Author: User
Tags wrappers

There has been a lot of talk lately about how deferreds shocould be implemented in jquery and the strong belief they shocould be implemented following Kris zyp's promise/a proposal.

I'm inbetween trains at the moment (Paris-> Brussels-> LONDON-> Brussels) So comparing point to point two very similar APIs (promise/a and, well, let's dub current jquery's take on it promise/J) is not something I intended to do right now. but time is of the essence, right?

Since I don't have that much essence in store, I'll make some general observation and then present what I think is a reasonably advanced use case. that way, everyone will be able to see where those two APIs differ and what benefits each may have.

It will also serve as some kind of pre-documentation paper that some people in the team may find useful and plugin developpers may be interested in.

From one to the other

One thing I always love to do when comparing APIs that are close enough (provided they are not some monstruous beasts the size of, say, DirectX and OpenGL ), is to try and write one into the other. so, how do you pass from a promise/a to a promise/J:

   function toPromiseJ( promiseA ) {    function flattened( isSuccess , args ) {        var i,            length,            elem,            type;        for ( i = 0 , length = args.length ; i    

This adapter makes it clear how Promise/J handles the parameters of then and fail: it flattens the arguments array and filters out anything that is not a function (it can be a good idea to keep that in mind for some points in the use case to come).

Note also that this is not a perfect adapter, namely because Promise/A does not specify if a fullfilled (or failed) promise executes the callbacks right away whereas Promise/J ensures a specific order for consistency. Calls to then and fail are “atomic” in the sense that all the callbacks provided in a single call will be “pushed” before being executed no matter the promise’s state. This has some nice properties, especially when a callback attaches another callback to the promise. For instance, the following code:

 var str = "";promiseJ.then( function() {    promiseJ.then( function() {        alert( str );    } );    str += "A";},function() {    str += "B";} );

will alert “AB” whether the promise is already fullfilled or whether it gets fullfilled in the future. Simulating this with Promise/A is nearly impossible unless you rely on some setTimeout( func , 0 ) to execute the callbacks (and I’m not really sure if the actual behaviour of such a hack can be made consistent across browsers).

That being said, let’s see how we can pass from a Promise/J to a Promise/A:

   function toPromiseA( promiseJ ) {    return jQuery.extend( {} , promiseJ , {        then: function( fullfilledHandler , errorHandler ) {            promiseJ.then( fullfilledHandler ).fail( errorHandler );        }    } );}

This adapter is clearly more compact but don’t be fooled: it doesn’t say anything about the APIs other than the fact Promise/J seems more expressive than Promise/A, that’s all. Beside, then now accepts arrays which, while being compatible, is not strictly compliant with Promise/A. The progressHandler has been left out intentionally but if it proved useful, it wouldn’t be a real hassle to add a progress method to Promise/J (though how these callbacks are to be called, with what value and how often is something Promise/A keeps in the dark — Should at least the value provided to the callbacks be specified? Maybe a percentage? A float? A status text? Really, it’s a tricky subject where Promise/A doesn’t take side and rightfully so given how implementation sensitive it could get).

add promises to an existing plugin

Now we start our use case. I decided to take the perspective of a plugin developer because, let’s face it, if we’re going to see how easy/uneasy such or such API may be, then we’d better stay away from “Hello World” examples. It’s clear the two pieces of code below are too close to one another to be any indicator:

   promiseA.then( successCallback, errorCallback );// vspromiseJ.then( successCallback ).fail( errorCallback );

If it was up to that small difference, then Promise/A it would be if only for immediate and free-of-charge interoparability with the libraries already implementing it.

Anyway, let’s go on with our use case. Anything based around the concept of two possible outcomes making use of an options map will do just fine. So let’s consider our wonderful and minimal $.confirm() plugin. This imaginary plugin was written before 1.5 and makes use of options to specify callbacks. So you would use it like this:

  jQuery.confirm( {    message: "Are you sure?",    ok: function() { /* ok case */ },    cancel: function() { /* cancel case */ }} );

Pre-1.5, a call to $.confirm() didn’t return anything. But since Neal Bamn, the author, really liked the changes in $.ajax() — yeah, I know, dream on! —, he decided it would be cool to use promises and add a bit of flexibility in there:

 var myConfirm = jQuery.confirm( {    message: "Are you sure?",    ok: function() { /* ok case #1 */ },    cancel: function() { /* cancel case #1 */ }} );// then, latermyConfirm.ok( function() { /* ok case #N */ } );myConfirm.cancel( function() { /* cancel case #N */ } );// or evenjQuery.when( jQuery.confirm( "Are you sure?" ) ).then( func );

How would he do it using Promise/A?

 jQuery.confirm = function( options ) {    var deferredA = jQuery.DeferredA(),        promiseA = deferred.getPromise(); // Inventing here since Promise/A doesn't specify this    // Do what's needed to show the confirm dialog    return jQuery.extend( {        ok: function( callback ) {            promiseA.then( callback );            return this;        },        cancel: function( callback ) {            promiseA.then( null , callback );            return this;        }    } , promiseA ).then( options.ok , options.cancel );};

Promise/A doesn’t specify how the promise is retrieved. Promise/J, on the other hand, does. Any object that can be observed using a promise must provide a “promise()” method that returns, you guessed it, a promise (this includes promises themselves, of course). For instance, jQuery.when uses the promise method as a mean to determine if its argument is observable or if a new Deferred must be created and immediately resolved with said argument. The promise method goes along the following lines:

 promise: function( object ) {    object = object || {};    // Attach "immutable" methods    // (then, fail, isResolved and isRejected)    return object;}

Promise/J takes an Aspect Oriented approach here in the sense you can attach the promise “behaviour” to any existing object. All the methods that do not return a specified value (here, a boolean) must return the object they are attached to (in short, return this). There is no way the promise method can be implemented using a prototype based approach and this is a very intentional design decision. Also of interest, the jQuery.Deferred object actually is a promise.

Note that, as of today, the promise method of a Promise/J is wrong in the sense that it ignores the argument and always returns itself. Expect this to be fixed shortly.

That being said, how would we implement these plugin enhancements with Promise/J:

    jQuery.confirm = function( options ) {    var deferredJ = jQuery.DeferredJ();    // Do what's needed to show the confirm dialog    return deferredJ.promise( {        ok: deferredJ.then,        cancel: deferredJ.fail    } ).ok( options.ok ).cancel( options.cancel );};

There are several key differences to note between the two implementations:

  1. Even if the Promise/A implementation made it possible to detach methods and attach them back to another object we would still need those anonymous functions. It’s due to the fact that, in Promise/A, then accepts an errorHandler as its second argument and it’s very doubtful that a user would expect the second argument of ok to be a cancel callback. If anything, he’d probably expect it to be considered as another ok callback.

  2. In the Promise/J implementation, new “aliases” are attached to an object on which the promise aspect is added. Here, ok and cancel are the exact same functions as then and fail respectively (think ===): since the most likely use case is one callback given to any of this methods at a time, then and fail will never recurse internally so what we have here is simpler but, also, faster code. As a nice side-effect, we can use ok and cancel with no overhead which makes the code somehow more readable.

  3. The return statement in the Promise/J version is a bit clearer — jQuery.extend() doesn’t tick as much as deferred.promise() —, but nothing prevents us from adding the promise helper method to the Promise/A version, so don’t be fooled (same goes for using the deferred directly instead of the promises).

  4. Since Promise/J then and fail methods do flatten their arguments and accept arrays, the ok and cancel options can now be arrays of callbacks, for free.

While we have a very simple case here, we could easily extend the concept to some wizard panel with a series of ok/cancel decisions where several promises are to be chained. It’s a good exercice to see how much each solution would grow in size and complexity.

   // Promise/AjQuery.twoPanelsA = function( options ) {    var deferredMain = jQuery.Deferred(),        deferredFirst = jQuery.Deferred(),        promiseMain = deferredMain.getPromise(),        promiseFirst = deferredFirst.getPromise();    // Do what's needed to show the wizard    return jQuery.extend( {} , promiseMain , {        okFirst: function( callback ) {            promiseFirst.then( callback );            return this;        },        cancelFirst: function( callback ) {            promiseFirst.then( null , callback );            return this;        }        ok: function( callback ) {            promiseMain.then( callback );            return this;        },        cancel: function( callback ) {            promiseMain.then( null , callback );            return this;        }    } ).then( options.ok , options.cancel )       .then( options.okFirst , options.cancelFirst )       .then( null , deferredMain.failed /*   

If you generalize this to all the plugins that would be prone to evolve to promise, Promise/A forces developers into an enormous amount of tedious anonymous functions wrappers. The downside of Promise/J is one more function call to attach options callbacks. On the other hand, since calling ok, cancel, okFirst or cancelFirst comes with no performance penalty, it gets tempting to generalize the code (jQuery.each is used here, but it’s obvious a for in loop would be faster and, thus, preferable):

  jQuery.each( "ok cancel okFirst cancelFirst".split( " " ) , function( _ , name ) {    promise[ name ]( options[ name ] );} );

You get the idea.

Of course, another approach would be to return one promise by panel and be a bit more “organized” but that would imply an intermediate object that provides methods to get access to said promises, actually breaking chainability (at least not without some convoluted code). It seems reasonable plugin developers will prefer to provide methods named after existing options, especially for plugins that already return an object (that was the case for jQuery.ajax for instance).

using the plugin

But let’s go back to Neal’s awesomely awesome 1.5 compliant new version of a plugin that’s just been uploaded (see what happens when I keep you occupied?). Corin Volser, a jQuery addict, grabs it as soon as it’s released. But it will come as no surprise to you that Corin’s boss is a clueless naysayer of massive proportions: it’s 7pm and he wants a journal of each users ok/cancel “clicks” now.

“This pug'in you talk about is no good to me if I don’t have that excel report on my desk by tomorrow morning!” he barks.

Worse of all, the boss demands an alert to pop out and the confirm window not to close if and when a user cancels “too often”. So logging on cancel must take place before any other cancel callback (I’ll spare you the spaghetti code involved backstage to prevent the confirm to close while the plugin does not support it).

How would Corin do this if Neal’s plugin was based on Promise/A?

  jQuery.loggedConfirm = function( options ) {    var originalCancel = options.cancel;    return jQuery.confirm( jQuery.extend( options , {        cancel: function() {            logger.cancel( options );            if ( jQuery.isFunction( originalCancel ) ) {                originalCancel.apply( this , arguments );            }        }    } ) ).ok( function() {        logger.ok( options );    } );};

Cumbersome and confusing: an intermediate variable, a jQuery.isFunction test, quite heavy on the plumbery. Let’s see if the Promise/J version is any better:

 jQuery.loggedConfirm = function( options ) {    return jQuery.confirm( jQuery.extend( options , {        ok: [ function() { logger.logOK( options ); }, options.ok ],        cancel: [ function() { logger.logCancel( options ); }, options.cancel ]    } ) );};

Still ugly, really, but at least the options testing is delegated to then and fail, so we lose the variable and now all log callbacks only contain a single statement which makes it a little more bearable (and more tempting to add the ok logger in the options for code consistency).

Damn these bosses, their Excels and their silly “special cases”.

best of both worlds?

Maybe it’s time for us to make a little pause and summarize what we saw so far:

  1. There are a lot of things in Promise/A that are left for implementors to decide. While it makes sense for some parts (like progress handlers which may, indeed, not even have their place in the spec at all), other parts would benefit from some clarifications and constraints (like callbacks execution order and mandatory “helper” methods).

  2. The code in the use case seemed generally cleaner using Promise/J than using Promise/A, but a lot is actually due a serious lack of specified helper methods on Promise/A’s end.

  3. However, the real issue is the fact Promise/A’s then method mixes success and error callbacks. While it makes sense in the simplest example, it comes at great cost (repetitive anonymous functions wrappers) when extending any legacy code which requires some flexibility in how things are named and published. The key point here is that Promise/A’s then method doesn’t really make much sense when not attached to the promise itself which makes it a poor candidate for Aspect Oriented programming. The Promise/J approach that clearly separates the two handlers types manages to limit the number of function redirections drastically (down to zero actually) while also enabling more elegant ways to deal with callbacks lists.

Now there’s been a lot of discussion about an intermediate solution that would solve some of the issues with Promise/A. This new interface (let’s call it Promise/A+) is actually the exact output of the Promise/J to Promise/A adapter we saw earlier:

 then: function( arrayOfSuccessHandlers , arrayOfErrorHandlers ),fail: function( errorHandler1, errorHandler2, ... )

The problem with Promise/A+ is that it doesn’t remove the mix of success and error handlers that de-facto precludes an Aspect Oriented approach which means the army of anonymous functions will still camp in your source-yard. Even worse, the Promise/A+ API is asymetrical with then and fail no longer having the same signature which is just plain counter-intuitive.

j@ubourg’s opinion

Well, the real issue that has been debated to death is “interoperability”. I really think a lot of people are confused about what it actually means. If interoperability meant “the same interface everywhere” then I think we would all code in a single, universal language. Interoperability has always been about “being able to communicate” which can very well be understood as “finding a bridge” between two systems.

So, in my humble opinion, I see providing an adapter as the best option. The Promise/J to Promise/A adapter is trivial to implement, is incredibly small once minified and could be delivered as an official plugin. I thought about having it inside jQuery core itself, but I believe it would be confusing for users to have 2 different promise interfaces right off the bat and would lead to some incredibly convoluted code mixing both. The kind of code you only see in #jquery.

Another solution would be to make Promise/J’s then compliant with Promise/A and to move the actual Promise/J’s then under another name (success? done?). However I’m really not a fan of this as I see it as potentially even more confusing than a dual-deferred offer. This is the solution that was chosen: the original Promise/J “then” method has been renamed as “done”. In jQuery 1.5, jQuery.Deferred provides a Promise/A-compliant “then” method. I’m more comfortable with the approach than I thought I would be when I wrote this very blog post.

One thing I know for sure is that Promise/J seems to be helpful in reducing user code size and complexity, while Promise/A seems oddly unadapted to a language like Javascript were functions are prime citizens and AOP is such a performance and hassle saver. It’s funny how big a change in users' code such tiny a difference in an interface can make.

afterword

So here we are, 11pm passed, and I have to prepare my bag and wake up early tomorrow morning. So, it’s quite obvious you’ll find typos, gigantic mistakes and things I simply forgot in this post. Still, I hope you’ll get a better understanding of how jQuery’s current deferred implementation works and why it was designed the way it was.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.