Y.Deferred, yo. Let's make this happen! #241

Closed
wants to merge 10 commits into
from

Conversation

Projects
None yet
10 participants
Contributor

lsmith commented Sep 6, 2012

Adds Y.Deferred and Y.Promise in 'deferred' module and features and Y.when (like Y.Parallel) in 'deferred-extras'.

Aimed to serve as the standard API for a transaction object/layer for big IO refactor. API feedback still welcome, though there was much discussion lsmith#36.

lsmith added some commits Aug 30, 2012

@lsmith lsmith Initial drop Y.Deferred and Y.Promise
With some real tests, some stubbed tests. Basic functionality
seems to be working, but I haven't been very rigorous about
it yet.
8c80d47
@lsmith lsmith Added hacker-friendly version + extras
New version uses prototypes and gentleman's privates, and
is broken into two pieces:
1. `deferred` which offers Y.Deferred and Y.Promise with only
   deferred.then(), resolve(), reject(), promise(), and getStatus(),
   and promise.then() and promise().
2. `deferred-extras` which adds deferred/promise.onProgress() and
   deferred.notify() for progress updates, an promise.wait() to
   insert a delay in the chained sequence.

Also added `deferred-when` which creates a `Y.when()` method to wrap
multiple operations and returns a promise that will be resolved when
all individual operations complete.
79037da

I think we want something like:

    promise: function (promise) {
        // check for instanceof?
        if (promise) {
             promise._deferred = this;
             this._promise = promise;
        }
        return this._promise;
    }

And maybe let it replace the promise only once.

lsmith added some commits Sep 4, 2012

@lsmith lsmith Bug fixes, incorporating @juandopazo's feedback
Split up the code into two modules:
deferred - core implementation
deferred-extras - +onProgress/notify, wait, and Y.when
17ad153
@lsmith lsmith All callbacks async now. when() fixed+fails fast
Plus when() now supports passing simple values as well as
functions and promises.  Previously, it would never resolve
the allDone deferred if a simple value was passed.
3c13c95
@lsmith lsmith Bind fallback then() callbacks to the deferred 7df81fb
@lsmith lsmith Add missing oop requirement to meta a02d502
@lsmith lsmith Add tests. deferred has 100% coverage, need extras 712d3e6
@lsmith lsmith when() bug fix & disallow onProgress for resolved
Tests yay!
203c7f0
@lsmith lsmith 100% coverage for extras, +more tests for deferred 01a9f33
Member

juandopazo commented Sep 7, 2012

Fantastic! The only thing missing is what I mention in that comment I left: a mechanism to return objects extending Y.Promise so that we can have a Y.Transaction class for IO that extends Y.Promise, a NodeDeferred plugin or any other creative use we come up with.

@ericf ericf commented on the diff Sep 10, 2012

src/deferred/js/deferred.js
+Represents an operation that may be synchronous or asynchronous. Provides a
+standard API for subscribing to the moment that the operation completes either
+successfully (`resolve()`) or unsuccessfully (`reject()`).
+
+@class Deferred
+@constructor
+**/
+function Deferred() {
+ this._subs = {
+ resolve: [],
+ reject : []
+ };
+
+ this._promise = new Y.Promise(this);
+
+ this._status = 'in progress';
@ericf

ericf Sep 10, 2012

Owner

Would a single word status be better? "progress".

@lsmith

lsmith Sep 10, 2012

Contributor

Maybe. I'm open to it, if you think it's important. To me, "progress" implies something has happened, which isn't necessarily the case. "unfulfilled" would be more appropriate, but is an odd word and would be typoed.

@ericf

ericf Sep 11, 2012

Owner

I think a single word is important, and I agree that English sucks.

@bwg

bwg Sep 11, 2012

I agree that a single word would be best. pending?

@jenny

jenny Sep 11, 2012

Owner

I can't resist a good naming discussion. "inprogress"?

@msweeney

msweeney Sep 11, 2012

Contributor

¿progresando?

@davglass

davglass Sep 11, 2012

Owner

"bobbingforapples"

@rgrove

rgrove Sep 11, 2012

Contributor

Sold! You had me at "¿".

@solmsted

solmsted Sep 15, 2012

Contributor

I tend to like pending or unfulfilled.

@ericf ericf commented on the diff Sep 10, 2012

src/deferred/js/deferred.js
+ var then = new Y.Deferred(),
+ promise = this.promise(),
+ resolveSubs = this._subs.resolve || [],
+ rejectSubs = this._subs.reject || [];
+
+ function wrap(fn, method) {
+ return function () {
+ var args = slice.call(arguments);
+
+ // Wrapping all callbacks in setTimeout to guarantee
+ // asynchronicity. Because setTimeout can cause unnecessary
+ // delays that *can* become noticeable in some situations
+ // (especially in Node.js), I'm using Y.soon if available.
+ // As of today, Y.soon is only available in the gallery as
+ // gallery-soon, but maybe it could get promoted to core?
+ (Y.soon || setTimeout)(function () {
@ericf

ericf Sep 10, 2012

Owner

This would use process.nextTick in Node.js?

@lsmith

lsmith Sep 10, 2012

Contributor

If the implementation of Y.soon does, yes. And gallery-soon does use it. Speaking of which, maybe it should be pulled into core :)

@ericf

ericf Sep 11, 2012

Owner

Oh Y.soon does a hole bunch of stuff. I'd like to look into why it would matter and what it would give you over setTimeout(function () {}, 0).

@juandopazo

juandopazo Sep 12, 2012

Member

It tries to do the shortest delay possible by using postMessage and other crazy hacks. I think it may be overkill. The only thing I can think about right now that may need faster timeouts in the browser are animations and those can use requestAnimationFrame. However Y.soon could be a nice abstraction over process.nextTick || setImmediate || setTimeout.

@ericf

ericf Sep 12, 2012

Owner

Agreed. I like the idea of Y.soon, but I think a more pragmatic implementation than its current one is the right balance. Unless of course there's any real reason for wanting faster callbacks that the hacks provide.

@solmsted

solmsted Sep 15, 2012

Contributor

Y.soon is probably a whole different topic for discussion but I'll try to be brief with comments here.

I feel the speed benefits are worth it.
http://jsperf.com/y-soon-vs-settimeout

I've read the timers used by setTimeout are expensive in other ways. I haven't tested this myself so this might be inaccurate. Depending on implementation, they can consume a bunch of memory, drain mobile device and laptop batteries, and add overhead to every turn of the JavaScript event loop. I feel like any of the implementations of Y.soon from gallery-soon (other than the fallback) would be less expensive, but I haven't done any actual performance comparisons to validate this claim.

As promises are intended to be a low level utility, performance should be a consideration. I can imagine a reasonably complex app chaining a dozen small promises for one unit of io. Then if the app does tons of near real time io, the cost of all the throw-away timers is going to add up.

I'd like to refactor Y.soon into a bunch of submodules and use conditional loading to decide one implementation to load. Then developers could choose to bypass conditional loading and use a specific implementation like MessageChannel, instead of it being completely wasted bytes. It's annoying that gallery-soon depends on node-base, especially in Node.js.

Y.soon could be brought into core by defining it as the fallback implementation using setTimeout 0. The gallery will continue to provide alternative implementations to replace that one.

@rgrove

rgrove Sep 15, 2012

Contributor

There may be very specific use cases in which the extreme hackery in gallery-soon is worthwhile, but as a general-purpose utility in YUI itself, it would give me the willies. It's the most micro of micro-optimizations, and I don't think we should encourage its use by anyone who doesn't completely understand its black magicks.

It's clever as hell, but it's also relatively large for such a simple purpose, and it's edge case city. Maintaining browser compat and ensuring proper test coverage seems like it would be a nightmare.

@solmsted

solmsted Sep 15, 2012

Contributor

I think I may have failed to explain myself clearly. I'd like to move the discussion of Y.soon here: http://yuilibrary.com/forum/viewtopic.php?f=18&t=10636

@ericf

ericf Sep 19, 2012

Owner

Just to make sure this is captured here, I added some thoughts to the Y.soon() forum thread about us possibly wanting to use one of the hacks so that callbacks will be called async, but before the next DOM repaint.

@ericf ericf commented on the diff Sep 10, 2012

src/deferred/js/deferred.js
+ };
+
+ this._promise = new Y.Promise(this);
+
+ this._status = 'in progress';
+
+}
+
+Y.mix(Deferred.prototype, {
+ /**
+ Returns the promise for this Deferred.
+
+ @method promise
+ @return {Promise}
+ **/
+ promise: function () {
@ericf

ericf Sep 10, 2012

Owner

Why a method? Why not just reference deferred.promise?

If this remains a method, should getPromise() be used?

@lsmith

lsmith Sep 10, 2012

Contributor

It seemed like a convention when reviewing some of the other deferred/promises libs out there, but the field is much broader than I'd initially found. I'm taking jQuery's implementation as strong convention for the sake of interoperability (or near-interop). jq has a promise() method to return the promise, though I suspect it is implemented as a method because it accepts an object parameter to decorate that object with the promise API. @juandopazo has asked for this same support, but I'm on the fence.

@juandopazo

juandopazo Sep 11, 2012

Member

I want promise objects with more methods, extended through prototypes. Otherwise promises are just a bloated version of Parallel

@ericf

ericf Sep 11, 2012

Owner

Isn't jQuery just using their standard accessor/mutator pattern for promise(), effectively the method does what getPromise() and setPromise() would be doing?

@juandopazo

juandopazo Sep 11, 2012

Member

In jQuery promise() does parasitic inheritance on any object, adding lots of methods to it.

// Get a promise for this deferred
// If obj is provided, the promise aspect is added to the object
promise: function( obj ) {
    return typeof obj === "object" ? jQuery.extend( obj, promise ) : promise;
}
@solmsted

solmsted Sep 15, 2012

Contributor

I'm pretty sure we will want the ability to customize the interfaces available on promises. The addMethod method may be good enough but I kind of like this prototype inheritance idea.

Also note that the Promises/D proposal defines the promiseSend method. It achieves something similar to addMethod, but uses a concept of message passing rather than method invocation. I personally don't really like the functionality of promiseSend, but I do like this interoperability side-effect: In the context of a promise library, the existence of a "promiseSend" method must be sufficient to distinguish a promise from any other value.

Although it only promotes interoperability if other libraries also implement it which I doubt many have.

@ericf ericf and 1 other commented on an outdated diff Sep 10, 2012

src/deferred/js/deferred.js
+ // asynchronicity. Because setTimeout can cause unnecessary
+ // delays that *can* become noticeable in some situations
+ // (especially in Node.js), I'm using Y.soon if available.
+ // As of today, Y.soon is only available in the gallery as
+ // gallery-soon, but maybe it could get promoted to core?
+ (Y.soon || setTimeout)(function () {
+ var result = fn.apply(promise, args),
+ resultPromise;
+
+ if (result && typeof result.promise === 'function') {
+ resultPromise = result.promise();
+
+ if (resultPromise.getStatus() !== 'in progress') {
+ then[method].apply(then, resultPromise.getResult());
+ } else {
+ result.promise().then(
@ericf

ericf Sep 10, 2012

Owner

You already have resultPromise cached above.

@lsmith

lsmith Sep 10, 2012

Contributor

It's declared, not assigned. Am I missing the line you're referring to?

@ericf

ericf Sep 11, 2012

Owner

Yes, L147.

@ericf ericf and 1 other commented on an outdated diff Sep 10, 2012

src/deferred/js/deferred.js
+ @method then
+ @param {Function} [callback] function to execute if the Deferred
+ resolves successfully
+ @param {Function} [errback] function to execute if the Deferred
+ resolves unsuccessfully
+ @return {Promise} The promise of a new Deferred wrapping the resolution
+ of either "resolve" or "reject" callback
+ **/
+ then: function (callback, errback) {
+ var then = new Y.Deferred(),
+ promise = this.promise(),
+ resolveSubs = this._subs.resolve || [],
+ rejectSubs = this._subs.reject || [];
+
+ function wrap(fn, method) {
+ return function () {
@ericf

ericf Sep 10, 2012

Owner

This method is begging for code comments, it's a mind warp, and hard to follow what's happening without them.

@lsmith

lsmith Sep 10, 2012

Contributor

It is a mind warp.

@ericf

ericf Sep 11, 2012

Owner

I'd like to understand it more, can you add some comments to it next time you're in this code, if you can even remember what it's doing :P

@ericf ericf and 1 other commented on an outdated diff Sep 10, 2012

src/deferred/js/deferred.js
+ `functionA().then(functionB).then(functionC)` where `functionA` returns
+ a promise, and `functionB` and `functionC` _may_ return promises.
+
+ @method then
+ @param {Function} [callback] function to execute if the Deferred
+ resolves successfully
+ @param {Function} [errback] function to execute if the Deferred
+ resolves unsuccessfully
+ @return {Promise} The promise of a new Deferred wrapping the resolution
+ of either "resolve" or "reject" callback
+ **/
+ then: function (callback, errback) {
+ var then = new Y.Deferred(),
+ promise = this.promise(),
+ resolveSubs = this._subs.resolve || [],
+ rejectSubs = this._subs.reject || [];
@ericf

ericf Sep 10, 2012

Owner

Were these suppose to be a conditional assignment? i.e:

subs        = this._subs,
resolveSubs = subs.resolve || (subs.resolve = []),
rejectSubs  = subs.reject || (subs.reject = []);
@lsmith

lsmith Sep 10, 2012

Contributor

Nope, byte shaving. When resolved, the only subscriptions supported are those corresponding to whether it was resolve()d or reject()ed. See line 78, for example. So if you call then() after it was resolved, it will immediately resolve the new deferred, or do nothing. The empty array allows the rest of the function body to run without error, but is never referenced, so it's thrown away at the end of the function, making it a no-op.

@ericf ericf commented on the diff Sep 10, 2012

src/deferred/js/promise.js
+}
+
+/**
+Adds a method or array of methods to the Promise prototype to relay to the
+so named method on the associated Deferred.
+
+DO NOT use this expose the Deferred's `resolve` or `reject` methods on the
+Promise.
+
+@method addMethod
+@param {String|String[]} methods String or array of string names of functions
+ already defined on the Deferred to expose on
+ the Promise prototype.
+@static
+**/
+Promise.addMethod = function(methods) {
@ericf

ericf Sep 10, 2012

Owner

Wha what? Should this be public?

@lsmith

lsmith Sep 10, 2012

Contributor

Public to allow features to be added to Promise.prototype rather than needing to subclass both Y.Deferred and Y.Promise to add features. Made for less, and less complex code. shrug

@ericf ericf and 2 others commented on an outdated diff Sep 10, 2012

src/deferred/js/when.js
+@module deferred
+@submodule deferred-when
+**/
+
+/**
+Wraps any number of callbacks in a Y.Deferred, and returns the associated
+promise that will resolve when all callbacks have completed. Each callback is
+passed a Y.Deferred that it must `resolve()` when that callback completes.
+
+@for YUI
+@method when
+@param {Function|Promise} operation* Any number of functions or Y.Promise
+ objects
+@return {Promise}
+**/
+Y.when = function () {
@ericf

ericf Sep 10, 2012

Owner

Would this replace Y.Parallel?

@lsmith

lsmith Sep 10, 2012

Contributor

It's more code than Y.Parallel and does more. Better that IO, et al, are refactored to output transactions based on promises. In which case, Y.when would be preferable for that/those specific use case(s). If the seed has a specific use case, then it's fine for Y.Parallel to also exist. Things like grouping event subscriptions could go either way (not "transaction" related, per se, but could be considered to be, based on the events as independent/parallel steps in an operation).

@juandopazo

juandopazo Sep 11, 2012

Member

It's Parallel with success, failure and progress :)

@ericf ericf and 1 other commented on an outdated diff Sep 10, 2012

src/deferred/js/when.js
+
+ function oneDone(i) {
+ return function () {
+ var args = slice.call(arguments);
+
+ results[i] = args.length > 1 ? args : args[0];
+
+ remaining--;
+
+ if (!remaining && allDone.getStatus() !== 'rejected') {
+ allDone.resolve.apply(allDone, results);
+ }
+ };
+ }
+
+ Y.Array.each(funcs, function (fn, i) {
@ericf

ericf Sep 10, 2012

Owner

Seems like using a for-loop here would be better for performance.

@lsmith

lsmith Sep 10, 2012

Contributor

Used to avoid incrementer leaking in the closure, necessitating a wrapping function anyway, so it seemed moot.

@ericf ericf and 3 others commented on an outdated diff Sep 10, 2012

src/deferred/js/when.js
+ };
+ }
+
+ Y.Array.each(funcs, function (fn, i) {
+ var finished = oneDone(i),
+ deferred;
+
+ // accept promises as well as functions
+ if (typeof fn === 'function') {
+ deferred = new Y.Deferred();
+
+ deferred.then(finished, failed);
+
+ // It's up to each passed function to resolve/reject the deferred
+ // that is assigned to it.
+ fn.call(Y, deferred);
@ericf

ericf Sep 10, 2012

Owner

Why Y? Do we need to apply a specific context?

@lsmith

lsmith Sep 10, 2012

Contributor

Arbitrary. I chose Y for lack of a better option, since I prefer not to execute from the global context. But maybe that's fine?

@rgrove

rgrove Sep 10, 2012

Contributor

Stick with the global context, imho. There are a few other random places in the lib that use Y as a default context, but I find this confusing and would prefer to stamp them out.

@davglass

davglass Sep 11, 2012

Owner

There are a few options here:

Global: forcing the user to bind their callbacks first.

Config: support a context option, done elsewhere that falls back to Y

Self: make the context the Deferred object.

Mixed: config with default being global or deferred (I like deferred).

Global is ok, but the user needs to know that. But the context config
seems to give the most since its configurable & gives the implementor
more control without the bind overhead.

Just my $0.02.

@lsmith

lsmith Sep 11, 2012

Contributor

I'm fine changing it to global.

The current signature, again based on jq, accepts n args, all treated as callbacks (or promises or result values), so there's no room for a config object unless the signature is changed.

Promises/B defines a signature for when() as when(value, callback, errback), which handles only one promise, so more closely resembles Promises/A promise.then(callback, errback).

If I were to alter the signature for when(), I'd be more inclined to change it to Y.when( <array of callbacks, etc> [, callback [, errback]]). The array of callbacks/promises would be wrapped in a tracking deferred as the current implementation is. If no callback or errback is passed, the wrapping deferred's promise would be returned. If either were included, it would internally call wrapperPromise.then(callback, errback) and return the resulting promise from that then() chain.

Owner

ericf commented Sep 10, 2012

@lsmith This stuff looks like it will be a great API to build data-access stuff on!

I'm wondering about a couple of things…

  1. Do you look at Y.Deferred as a lower-level tool that wouldn't end up being exposed to the main end uses of YUI? I worry that it detracts from the YUI-way of doing things by not using events and attributes. That said I think it's important for a low-level API to remain lightweight.
  2. Double vs. single callback API. I'd like the discuss further the differences between using a Node.js style callback(err, data) API vs. the callback and errorback functions.

Also, I should probably do some deeper reading on the deferred/promise stuff, do you have any good links to share?

Contributor

lsmith commented Sep 11, 2012

  1. The primary interface is the then(callback, errback) function of the promise and the resolve() and reject() methods on the deferred. Anything else is sugar. That said, I am still holding on to my initial implementation that used on('progress', callback) instead or onProgress(callback), which then bled over to promise.on(<"resolve" or "success" or "done">, callback) (and so for "reject", "failure", etc), which could be expanded per class specialization. But as an abstract API, then(), resolve(), and reject() would be the baseline, and be public. But yes, I really didn't want to require custom events for this layer because of the module dep weight and code complexity. Deferreds really should be a simple, (fairly?) inflexible API.
  2. I thought we'd talked about using Node.js style callback(err, data) for the transport layers, promises for transactions, and classes for transaction factories (aka DataSource or Resource). That's still my thought. Transport callbacks would dovetail into transactions by forking err => reject().
@lsmith lsmith Add docs, API feedback from review, tests
* More docs in then()
* errback that doesn't return a failed promise will continue to next callback,
  not next errback.  Signals recovery from the failure.
* Y.when renamed Y.batch
* New Y.when(promiseOrValue, callback, errback) added
* New Y.defer(callback(deferred)) added
* Tests added for new APIs and changed APIs
04834ed

ericf referenced this pull request Oct 11, 2012

Merged

Async faster with Y.soon! #304

Owner

ericf commented Oct 15, 2012

Here's a good post about the true point of Promises:
https://gist.github.com/3889970

Contributor

lsmith commented Oct 15, 2012

Yep, and it supports my want for keeping the Deferred and Promise separate. I am technically exposing the promise's underlying deferred via promise._deferred, which I had issue with, but seemed pragmatic instead of building a new object with unique methods closing over the deferred each time. The success and exception flow is supported in my implementation, though I don't think I'm handling literal exceptions via try/catch. He made no comment on the relevance or importance of progress handlers, as seems to be the usual case.

Member

juandopazo commented Oct 15, 2012

Nice article! I found a possible bug based on that article:

// foo.json contains {items:[1,2,3]}
Y.io.get('foo.json')
    .then(function (id, xhr) {
        return JSON.parse(xhr.responseText);
    })
    .then(function (data) {
        return data.items.map(Math.sqrt);
    })
    .then(function (roots) {
        console.log(roots);
    }, function (id, xhr) {
        console.error(xhr);
    });

This works ok until returning an array from the second then(). Then it turns the array into parameters, so roots ends up being 1 instead of [1, 1.4142135623730951, 1.7320508075688772].

Maybe assuming an array means an argument list is not a good idea.

@juandopazo juandopazo commented on the diff Oct 15, 2012

src/deferred/js/extras.js
+ of operations after the inserted pause.
+
+ @method wait
+ @param {Number} ms Number of milliseconds to wait before resolving
+ @return {Promise}
+ **/
+ wait: function (ms) {
+ var deferred = new Y.Deferred(),
+ promise = deferred.promise(),
+ timeout;
+
+ // || 0 catches 0 and NaN
+ ms = Math.max(0, +ms || 0);
+
+ this.then(function () {
+ var args = slice.call(arguments);
@juandopazo

juandopazo Oct 15, 2012

Member

No need to call slice here.

@lsmith

lsmith Oct 15, 2012

Contributor

It is necessary to pass the then() args into the setTimeout function.

@juandopazo

juandopazo Oct 15, 2012

Member

I mean you can just do var args = arguments.

@lsmith

lsmith Oct 15, 2012

Contributor

Oh duh! Right you are :)

@juandopazo

juandopazo Oct 15, 2012

Member

There are a couple of those around. Must.save.CPU.cycles.

Owner

ericf commented Oct 16, 2012

Another data point, RSVP lib by Yehuda Katz and Tom Dale:
https://github.com/tildeio/rsvp.js

Contributor

lsmith commented Oct 16, 2012

Another implementation that ignores progress events. There's a discussion going on in the cujojs google group about propagating progress events down the promise chain. I seem to be on the "grumpy old man" side. shrug
https://groups.google.com/forum/?fromgroups=#!topic/cujojs/QurUiAeTa5E

Contributor

lsmith commented Oct 24, 2012

Update: I've been chatting with @briancavalier, who is working on better defining the Promises/A spec (yay!), and doing more research. This implementation will change, but I don't anticipate much change in the API.

Look forward to more commits in the PR.

ericf was assigned Oct 25, 2012

Member

juandopazo commented Dec 6, 2012

Hey @lsmith do you think we should rewrite Domenic's test suite for YUI Test or can we run it as a CLI test?

Member

juandopazo commented Jan 10, 2013

Now that the promises A+ spec is stable.I drafted an implementation that implements that spec following the YUI style. I'm currently writing unit tests.

You can find it in a pull request in my own fork of YUI: juandopazo#4

There are some open issues:

  • Promises require Y.soon which means requiring “timers” every time we want promises
  • For tests, then() has a problem because it catches errors and so Y.Assert functions don’t work. @ericf mentioned the possibility of a workaround with YUI Test. Otherwise we may need end() a method that causes promises to throw errors
  • I still need to figure out how to return different types of promises without creating too many of them. See this gist to get an idea why this is an issue: https://gist.github.com/4506753
  • I need to translate the spec tests to YUI tests. This duplicates efforts but provides coverage. Should we run both versions of the spec tests?
  • Having compatibility with WinJS promises with a syntax like new Promise(function (fulfill, reject) {}) means creating two more functions per promise by using bind, and now that we're completely hiding the resolver from the promise by closing over it with instance properties we may want to avoid creating more functions and just pass the resolver to that function. Basically:
var promise = new Y.Promise(function (fulfill, reject) {
  fulfill('yay!');
});

// vs

var promise = new Y.Promise(function (resolver) {
  resolver.fulfill('yay!');
});
Contributor

lsmith commented Jan 12, 2013

Issue responses:

  • What specifically are you concerned about? Timers is small, so not size. It's another function hop between promises and setImmediate, but that cost is unavoidable unless we want to standardize on setTimeout, which we don't.
  • I'm curious about the specific cases where this causes trouble. I don't think we need end(), though it would make a reasonable extras module/feature.
  • commented on the gist. Not sure this is a problem.
  • I'd say keep the non-YUI Test version of the tests in tests/manual
  • I'm partial to the function (fulfill, reject) syntax, and not concerned about the private functions. I don't think you need to use bind. You could do away with the Resolver class and set this._resolver = { fulfill: function ()..., reject: function ()... }; if necessary. This point warrants more discussion, though, for sure.
Member

juandopazo commented Jan 12, 2013

Timers: you brought it up last time we chatted about this. I'm not that concerned. I'd like to have Y.Get return promises and that would mean bundling timers and promises in yui.js, but I can live with it being optional.

end: I only ran into this when writing tests. For instance:

'some asynchronous test for promises': function () {
  var test = this;

  Y.when(5).then(function (value) {
    // pausing and resuming work ok
    test.resume();

    // Assert.areEqual will throw an error which will be caught
    // by then() and reject this promise instead, so Y.Test
    // doesn't receive the assertion
    Y.Test.Assert.areEqual(4, value, 'value should be 4');
  });
  test.wait();
};

I absolutely prefer not to add end. I'll look at the internals of Y.Test to see if there's a workaround. Eric said something about there being an option that could help.

Syntax: I also like function (fulfill, reject). I don't usually worry about memory performance but since it's really easy to create a lot of Promise objects it's something to consider. Plus, there are cases in which you may want to use other methods from the resolver like getStatus. How would you expose them? At the A+ group, there's the proposal of having fulfill contain extra methods. But that means even more closures instead of prototype functions.

Member

juandopazo commented Jan 13, 2013

Another issue: I'm not sure Y.batch should treat functions differently. There isn't much difference between Y.batch(fn) and Y.batch(Y.Promise(fn)). Maybe it made sense when we had to create a Deferred object first. And some people might want to return functions for some reason. It's JavaScript and it has functions as values after all. I think we should minimize magic here.

Hey guys, you've probably been following the resolver API convo over at Promises/A+, but just wanted to give a heads up that "fulfill" as a verb is being called into question. I'm currently against it, as are others, at least as it's been described to this point. I am strongly in favor the behavior of when.js and Q's resolve() (even if we change the name).

There's a new thread on the name here

Member

juandopazo commented Jan 13, 2013

Nevermind about tests and end. I totally forgot how to use pause and resume in YUI Test. I was doing test.resume(); Assert.foo() instead of test.resume(function () { Assert.foo() });. * facepalm *

@briancavalier thanks for the heads-up! I agree that not being upfront with what fulfill does could lead to unexpected errors. For the others to follow what Brian says, here's an example:

// the identity function should work in any `then` callback
function identity(x) {
  return x;
}
// for example:
Y.when(5).then(identity).then(function (value) {
  console.log(value); // 5!
});

// however, if the value is a promise, it can give you an unexpected result
var promise = Y.Promise(function (fulfill) {
  // promise holds a promise for 5 as a value (a promise inside a promise)
  fulfill(Y.when(5));
});
// now we create a new promise going through identity
promise.then(identity).then(function (value) {
  // but we get 5, not a promise, because identity returned a promise to 
  // then() and then() does special stuff to promises
  console.log(value); // 5!
});
Contributor

lsmith commented Jan 14, 2013

@juandopazo less magic for functions passed to Y.batch is fine. I honestly don't follow what the confusion is that you're trying to illustrate in your last code snippet. It all looks like expected behavior to me. Maybe I'm too close to it.

@briancavalier Thanks for the heads up. I've been remiss in keeping up with the org discussions, but will catch up tomorrow (Monday).

Contributor

solmsted commented Jan 14, 2013

@juandopazo I don't quite understand what you are saying about Y.batch. Currently it accepts functions or promises as arguments, are you saying that you want it to only accept promises? If so, I'd like to argue for you to keep it the way it is.

Y.batch is very similar to the API I wrote for Y.Async.runAll which I have really enjoyed using. It's true that

Y.batch(fn);

is not much different to

Y.batch(Y.Promise(fn));

but when your code starts to include dozens of promises, something like

Y.batch(function (resolve) {
    Y.batch(someFn, someFn, someFn, someFn, someFn).then(resolve);
}, function (resolve) {
    Y.batch(someFn, someFn, someFn, someFn, someFn).then(resolve);
}, function (resolve, reject) {
    Y.batch(someFn, someFn, someFn, someFn, someFn).then(resolve, reject);
}, function (resolve, reject) {
    Y.batch(someFn, someFn, someFn, someFn, someFn).then(resolve, reject);
}, anotherFn, anotherFn, anotherFn);

and if you have many such bits of code within a project, you really start to appreciate not having to repeatedly type new Y.Promise(...). Every repetition of new Y.Promise(fn) is replaced by the one line typeof fn === 'function' ? new Y.Promise(fn) : fn and I think that's a win.

I would even go so far as to say, you should allow new Y.Promise(otherPromise). If otherPromise is an instance of Y.Promise, just return otherPromise. If otherPromise is some other Promises A+ compatible promise object it should return a new promise that would resolve when the other promise resolves, reject when the other promise rejects, etc. This way, all new API's could accept either functions or any promises and they could be normalized to a Y.Promise by simply calling new Y.Promise(argument)

Member

juandopazo commented Jan 14, 2013

would even go so far as to say, you should allow new Y.Promise(otherPromise).

That's what Y.when is for. Y.when(5) will return a new promise fulfilled with the value 5. Y.when(promise) will just return the promise.

Currently Y.batch takes 3 different types of arguments: promises, functions and values. Promises and values (that are not functions) are passed through Y.when. So Y.batch(5) is equivalent to Y.batch(Y.when(5)). I didn't expect users to create that many promises in place. I expected them to do stuff like Y.batch(Y.io.js('foo.js'), Y.io.css('foo.css')). I know functions as values in this case is a rarity, but I want to simplify as much as possible around promises because they already have a substantial mental cost.

Contributor

solmsted commented Jan 14, 2013

I completely looked over the call to Y.when.

Okay, I see how that makes more sense, assuming you're working with other API's that return promises. Since YUI and Node.js don't currently have APIs that return promises, I'm very accustomed to using anonymous functions to wrap the async functionality everywhere. As YUI's use of promises grows, I'll be okay with Y.batch accepting promises or values. I'll always have gallery-async where I can hack in my own sugar.

Contributor

lsmith commented Jan 14, 2013

@solmsted another two points worth mentioning:

  1. Passing a function to Y.batch to get its promise wrapping magic is a convenience for what should be the uncommon case, but if Y.batch is used with a rash of anonymous functions, there's something wrong with the code. Most or all of those functions should be extracted out to methods, in which case, the function bodies can accept values and return promises.
  2. a function is a value, so treating functions differently makes it difficult to promise wrap functions as values rather than promise executors. The inconvenience is roughly equivalent, between Y.batch({wrapped:functionAsValue}, functionAsExecutor) and Y.batch(functionAsValue, Y.Promise(functionAsExecutor)), so it's more a matter of style and api-of-least-surprise.

I like the convenience, but it sacrifices a reasonable use case and promotes anon-fn hackery over more maintainable styles.

Contributor

solmsted commented Jan 14, 2013

@lsmith Yes I completely agree with your 2nd point. I failed to notice the call to Y.when and never considered that simple values would be passed to Y.batch. So I'm fine with Y.batch treating functions as values.

It doesn't seem necessary to add noise here but sometime in the future I'd like to resume a conversation about methods returning a promise vs anonymous callback functions.

juandopazo referenced this pull request Feb 11, 2013

Merged

A+ compatible Promises #445

Contributor

lsmith commented Feb 12, 2013

Closing this in favor of #445

lsmith closed this Feb 12, 2013

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment