goog.deferred.Deferred

This is a proposal for an alternative implementation of goog.async.Deferred named goog.deferred.Deferred.

vararg callbacks and errbacks?

One point of discussion is whether the callback() and errback() methods should be extended to take a variable number of arguments.

On one hand, this would be more convenient than having to package multiple arguments to a callback or errback in an object literal.

On the other hand, I think that the concept of a Deferred is incredibly important, to the point that I think there should be support for parameterization in the Closure Compiler's type system as there is for Array and Object. For example, the following are meaningful type expressions: Array.<number> and Object.<boolean>.

Therefore, ultimately I would like to see support for goog.deferred.Deferred.<A,B>, which would imply that the methods for that instance would become:

/**
 * @param {!function(A)} callback
 * @param {?Object=} scope
 */
goog.deferred.Deferred.prototype.addCallback = function(callback, scope) { //...


/**
 * @param {!function(B)} errback
 * @param {?Object=} scope
 */
goog.deferred.Deferred.prototype.addErrback = function(errback, scope) { //...


/**
 * @param {A} result
 */
goog.deferred.Deferred.prototype.callback = function(result) { //...


/**
 * @param {B} result
 */
goog.deferred.Deferred.prototype.errback = function(result) { //...

Arguably, if callbacks and errbacks take a single argument, this is much more intuitive. Also, goog.deferred.Deferred.<A> would be a shorthand for goog.deferred.Deferred.<A,A> (or maybe goog.deferred.Deferred.<A,*>).

New Methods

In addition to simplifying the API of goog.async.Deferred, goog.deferred.Deferred also introduces some new methods:

Please check them out and provide feedback.

Naming

The names of methods in this class are subject to debate. Personally, when I first started using goog.async.Deferred, I was really thrown by callback() vs. addCallback(). One option would be to refer to a Deferred whose value is set as determined, in which case callback() and errback() could become determineSuccess() and determineFailure(), respectively (though those names have their own drawbacks).

Admittedly, bind() and map() may be too easily confused with things like goog.bind() or goog.array.map().

addScopeCheckedCallback() is perhaps an uncomfortably long name.

DeferredList

Yes, if this implementation of Deferred is accepted, then a new implementation of DeferredList will also be provided.

Source Code

Below is the syntax-highlighted source code for goog.deferred.Deferred:

  1 /**
  2  * @fileoverview goog.deferred.Deferred is similar to goog.async.Deferred, but
  3  * has some important differences in its design:
  4  * <ul>
  5  *   <li>When errback(arg) is called, arg is not wrapped in an Error.
  6  *   <li>When callback(arg) is called and arg is an Error, the functions
  7  *       registered via addCallback() are called with arg rather than those
  8  *       registered via addErrback().
  9  *   <li>There is no automatic chaining when a callback or errback returns a
 10  *       Deferred.
 11  *   <li>There is no such thing as a canceller or defaultScope.
 12  * </ul>
 13  * The goal of these design changes is to make operations with
 14  * goog.deferred.Deferred more explicit (and hopefully less error-prone) than
 15  * when working with goog.async.Deferred.
 16  *
 17  * @author bolinfest@gmail.com (Michael Bolin)
 18  */
 19 goog.provide('goog.deferred.Deferred');
 20 goog.provide('goog.deferred.Deferred.State');
 21 
 22 goog.require('goog.array');
 23 goog.require('goog.asserts');
 24 goog.require('goog.functions');
 25 
 26 
 27 /**
 28  * @constructor
 29  */
 30 goog.deferred.Deferred = function() {};
 31 
 32 
 33 /** @enum {number} */
 34 goog.deferred.Deferred.State = {
 35   NO_RESULT: 1,
 36   SUCCESS: 2,
 37   ERROR: 3
 38 };
 39 
 40 
 41 /**
 42  * @type {!goog.deferred.Deferred.State}
 43  * @private
 44  */
 45 goog.deferred.Deferred.prototype.state_ =
 46     goog.deferred.Deferred.State.NO_RESULT;
 47 
 48 
 49 /**
 50  * @type {?Array}
 51  * @private
 52  */
 53 goog.deferred.Deferred.prototype.callbacks_;
 54 
 55 
 56 /**
 57  * @type {?Array}
 58  * @private
 59  */
 60 goog.deferred.Deferred.prototype.errbacks_;
 61 
 62 
 63 /**
 64  * @type {*}
 65  * @private
 66  */
 67 goog.deferred.Deferred.prototype.result_;
 68 
 69 
 70 /**
 71  * @param {!Function} callback
 72  * @param {?Object=} scope
 73  * @return {!goog.deferred.Deferred}
 74  */
 75 goog.deferred.Deferred.prototype.addCallback = function(callback, scope) {
 76   return this.addCallbacks_(callback, null /* errback */, scope);
 77 };
 78 
 79 
 80 /**
 81  * @param {!Function} errback
 82  * @param {?Object=} scope
 83  * @return {!goog.deferred.Deferred}
 84  */
 85 goog.deferred.Deferred.prototype.addErrback = function(errback, scope) {
 86   return this.addCallbacks_(null /* callback */, errback, scope);
 87 };
 88 
 89 
 90 /**
 91  * @param {!Function} callback
 92  * @param {!Function} errback
 93  * @param {?Object=} scope
 94  * @return {!goog.deferred.Deferred}
 95  */
 96 goog.deferred.Deferred.prototype.addCallbacks = function(
 97     callback, errback, scope) {
 98   return this.addCallbacks_(callback, errback, scope);
 99 };
100 
101 
102 /**
103  * @param {!Function} f
104  * @param {?Object=} scope
105  * @return {!goog.deferred.Deferred}
106  */
107 goog.deferred.Deferred.prototype.addBoth = function(f, scope) {
108   return this.addCallbacks_(f, f, scope);
109 };
110 
111 
112 /**
113  * @param {?Function} callback
114  * @param {?Function} errback
115  * @param {?Object=} scope
116  * @return {!goog.deferred.Deferred}
117  * @private
118  */
119 goog.deferred.Deferred.prototype.addCallbacks_ = function(
120     callback, errback, scope) {
121   scope = scope || null;
122 
123   // If this Deferred has already been determined, then simply call the
124   // appropriate function with the result and exit.
125   if (this.hasResult_()) {
126     var f = this.isSuccess_() ? callback : errback;
127     if (f) f.call(scope, this.result_);
128     return this;
129   }
130 
131   if (callback) {
132     var callbacks = this.callbacks_ || (this.callbacks_ = []);
133     callbacks.push(goog.bind(callback, scope));
134   }
135   if (errback) {
136     var errbacks = this.errbacks_ || (this.errbacks_ = []);
137     errbacks.push(goog.bind(errback, scope));
138   }
139   return this;
140 };
141 
142 
143 /**
144  * @return {boolean} whether this result has been determined
145  * @private
146  */
147 goog.deferred.Deferred.prototype.hasResult_ = function() {
148   return this.state_ != goog.deferred.Deferred.State.NO_RESULT;
149 };
150 
151 
152 /**
153  * @return {boolean}
154  * @private
155  */
156 goog.deferred.Deferred.prototype.isSuccess_ = function() {
157   return this.state_ == goog.deferred.Deferred.State.SUCCESS;
158 };
159 
160 /**
161  * @param {*} result
162  */
163 goog.deferred.Deferred.prototype.callback = function(result) {
164   this.setResult_(result, goog.deferred.Deferred.State.SUCCESS);
165 };
166 
167 
168 /**
169  * @param {*} result
170  */
171 goog.deferred.Deferred.prototype.errback = function(result) {
172   this.setResult_(result, goog.deferred.Deferred.State.ERROR);
173 };
174 
175 
176 /**
177  * @param {*} result
178  * @param {!goog.deferred.Deferred.State} state
179  * @private
180  */
181 goog.deferred.Deferred.prototype.setResult_ = function(result, state) {
182   if (this.hasResult_()) {
183     throw new Error('goog.deferred.Deferred already has a result');
184   }
185   this.state_ = state;
186   this.result_ = result;
187 
188   // If this is a SUCCESS, then the errbacks will never get called (and vice
189   // versa), so they should be dereferenced so they can be garbage collected.
190   var functions;
191   if (this.isSuccess_()) {
192     delete this.errbacks_;
193   } else {
194     delete this.callbacks_;
195   }
196 
197   this.fire_();
198 };
199 
200 
201 /**
202  * @private
203  */
204 goog.deferred.Deferred.prototype.fire_ = function() {
205   goog.asserts.assert(this.hasResult_());
206 
207   // Call each of the appropriate registered functions with the result.
208   var isSuccess = this.isSuccess_();
209   var result = this.result_;
210   var functions = isSuccess ? this.callbacks_ : this.errbacks_;
211   var exceptions;
212   if (functions) {
213     goog.array.forEach(functions, function(f) {
214       try {
215         f(result);
216       } catch (e) {
217         exceptions = exceptions || [];
218         exceptions.push(e);
219       }
220     });
221   }
222 
223   // Now that all of the registered functions have been called, delete all of
224   // their references.
225   if (isSuccess) {
226     delete this.callbacks_;
227   } else {
228     delete this.errbacks_;
229   }
230 
231   if (exceptions) {
232     goog.array.forEach(exceptions, this.throwException_, this);
233   }
234 };
235 
236 
237 /**
238  * @param {*} e
239  */
240 goog.deferred.Deferred.prototype.throwException_ = function(e) {
241   goog.global.setTimeout(function() { throw e; }, 0);
242 };
243 
244 
245 /**
246  * Assuming that this object is of type (Deferred A), it takes a function of
247  * (A -> B) and returns a new deferred of type (Deferred B):
248  *
249  * (Deferred A) -> (A -> B) -> (Deferred B)
250  *
251  * @param {!Function} callbackTransform
252  * @param {?Object=} scope
253  * @return {!goog.deferred.Deferred}
254  */
255 goog.deferred.Deferred.prototype.mapCallback = function(
256     callbackTransform, scope) {
257   return this.map(callbackTransform, goog.functions.identity, scope);
258 };
259 
260 
261 /**
262  * @param {!Function} errbackTransform
263  * @param {?Object=} scope
264  * @return {!goog.deferred.Deferred}
265  */
266 goog.deferred.Deferred.prototype.mapErrback = function(
267     errbackTransform, scope) {
268   return this.map(goog.functions.identity, errbackTransform, scope);
269 };
270 
271 
272 /**
273  * @param {!Function} callbackTransform
274  * @param {!Function} errbackTransform
275  * @param {?Object=} scope
276  * @return {!goog.deferred.Deferred}
277  */
278 goog.deferred.Deferred.prototype.map = function(
279     callbackTransform, errbackTransform, scope) {
280   scope = scope || null;
281   var deferred = new goog.deferred.Deferred();
282   this.addCallback(function(result) {
283     var transformedResult = callbackTransform.call(scope, result);
284     deferred.callback(transformedResult);
285   });
286   this.addErrback(function(result) {
287     var transformedResult = errbackTransform.call(scope, result);
288     deferred.errback(transformedResult);
289   });
290   return deferred;
291 };
292 
293 
294 /**
295  * Takes a deferred and binds it to a function that returns a deferred to
296  * produce a new deferred whose callbacks will receive the value from the new
297  * deferred that is produced by the function. This is perhaps better expressed
298  * in an ML-like syntax:
299  *
300  * (Deferred A) -> (A -> Deferred B) -> (Deferred B)
301  *
302  * @param {!function(*): !goog.deferred.Deferred} f that takes an argument of type
303  *     A and returns a goog.deferred.Deferred whose callbacks will receive an
304  *     argument of type B
305  * @param {?Object=} scope An optional scope to call function f in
306  * @return {!goog.deferred.Deferred} whose callbacks will receive an argument of
307  *     type B
308  */
309 goog.deferred.Deferred.prototype.bind = function(f, scope) {
310   var outDeferred = new goog.deferred.Deferred();
311   this.addCallback(function(value) {
312     var resultingDeferred = f.call(scope, value);
313     resultingDeferred.addCallbacks(
314         outDeferred.callback, outDeferred.errback, outDeferred);
315   });
316   return outDeferred;
317 };
318 
319 
320 /**
321  * Adds a callback to this Deferred with a scope object that extends
322  * goog.Disposable. When the value of this Deferred is supplied, it will only
323  * call the callback if the scope has not been disposed.
324  * @param {!Function} callback
325  * @param {!goog.Disposable} scope
326  * @return {!goog.deferred.Deferred}
327  */
328 goog.deferred.Deferred.prototype.addScopeCheckedCallback = function(
329     callback, scope) {
330   return this.addScopeCheckedCallbacks_(callback, null /* errback */, scope);
331 };
332 
333 
334 /**
335  * Adds an errback to this Deferred with a scope object that extends
336  * goog.Disposable. When the value of this Deferred is supplied, it will only
337  * call the errback if the scope has not been disposed.
338  * @param {!Function} errback
339  * @param {!goog.Disposable} scope
340  * @return {!goog.deferred.Deferred}
341  */
342 goog.deferred.Deferred.prototype.addScopeCheckedErrback = function(
343     errback, scope) {
344   return this.addScopeCheckedCallbacks_(null /* callback */, errback, scope);
345 };
346 
347 
348 /**
349  * Adds a callback and errback to this Deferred with a scope object that extends
350  * goog.Disposable. When the value of this Deferred is supplied, it will only
351  * call the callback if the scope has not been disposed.
352  * @param {!Function} callback
353  * @param {!Function} errback
354  * @param {!goog.Disposable} scope
355  * @return {!goog.deferred.Deferred}
356  */
357 goog.deferred.Deferred.prototype.addScopeCheckedCallbacks = function(
358     callback, errback, scope) {
359   return this.addScopeCheckedCallbacks_(callback, errback, scope);
360 };
361 
362 /**
363  * Adds a callback and errback to this Deferred with a scope object that extends
364  * goog.Disposable. When the value of this Deferred is supplied, it will only
365  * call the callback if the scope has not been disposed.
366  * @param {?Function} callback
367  * @param {?Function} errback
368  * @param {!goog.Disposable} scope
369  * @return {!goog.deferred.Deferred}
370  */
371 goog.deferred.Deferred.prototype.addScopeCheckedCallbacks_ = function(
372     callback, errback, scope) {
373   var scopeCheck = function(cb) {
374     return cb ? function(value) {
375       if (!scope.isDisposed()) {
376         cb.call(scope, value)
377       }
378     } : null;
379   };
380   return this.addCallbacks_(scopeCheck(callback), scopeCheck(errback));
381 };
382 
383 
384 /**
385  * @param {*} result
386  * @return {!goog.deferred.Deferred}
387  */
388 goog.deferred.Deferred.success = function(result) {
389   var deferred = new goog.deferred.Deferred();
390   deferred.callback(result);
391   return deferred;
392 };
393 
394 
395 /**
396  * @param {*} result
397  * @return {!goog.deferred.Deferred}
398  */
399 goog.deferred.Deferred.failure = function(result) {
400   var deferred = new goog.deferred.Deferred();
401   deferred.errback(result);
402   return deferred;
403 };
404