Permalink
Browse files

further development on promise implementation, discussed in README

  • Loading branch information...
1 parent 20d198d commit fb8d3016914efcb9aca07a41d04fc08d2785e4f7 @wizardwerdna committed Apr 3, 2013
Showing with 325 additions and 36 deletions.
  1. +111 −0 DISCUSSION.md
  2. +82 −17 README.md
  3. +1 −1 covenant.coffee
  4. +88 −10 index.js
  5. +1 −1 index.map
  6. +2 −2 package.json
  7. +39 −4 promise.coffee
  8. +1 −1 test/covenant.coffee
View
@@ -0,0 +1,111 @@
+## Discussion
+
+### Using the Promise
+
+A promise represents a `value`, that may exist now or in the future, or the `reason` why a value could not be computed. At any point in time, a promise will be either: (i) `pending` resolution; (ii) `fulfilled` with a `value`; or (iii) `rejected` with a `reason`. A pending object can be resolved with the `p.fulfill` and `p.reject` functions. Once resolved, any further call to either function is ignored. Resolution is not guaranteed, and a promise can remaining forever pending.
+
+A program performing an asynchronous computation may deliver its result by creating apromise and returning it to the client. The program then manages its state by fulfilling it with a value or rejecting it with a reason as may be required. For example, a promise for delivery of file contents might be built as follows:
+
+```coffeescript
+createReadFilePromise = (filename, encoding='utf8') ->
+ p = new Covenant
+ fs.readFile filename, encoding, (err, value) ->
+ if err
+ p.reject(err)
+ else
+ p.fulfill(value)
+ p
+```
+
+This pattern is common for node API calls with node callbacks. Once the promise is built, a client receiving the promise can query it by registering resolution handlers. The client may register as many handlers as the programmer likes, both before and after resolution. For example,
+
+```coffeescript
+createReadFilePromise('filename.txt').
+ then console.log, console.error
+```
+
+which will log the result value to stdout upon fulfillment, or writes the error to stderr upon rejection. Of course, this simply uses promises to do what the original API can easily do. While there are theoretical reasons why [promises are superior to direct callbacks](http://blog.jcoglan.com/2013/03/30/callbacks-are-imperative-promises-are-functional-nodes-biggest-missed-opportunity/), the practical reasons are wonderful enough:
+
+### The Pyramid of Doom and Chained Promises
+
+Imagine that we have a callback-based function that takes a url and returns its data or an error message. One Url produces information about a particular user, including an url for user's posts, which in turn contains information about comments related to that post. We want to get date for user Jim's most recent post and all of its comments. With traditional callbacks, this might be written
+
+```coffeescript
+http.get('/users/Jim', (err, jim) ->
+ if (err)
+ # handle an error trying to get jim's info
+ else
+
+ # do some stuff for jim
+ http.get(jim.mostRecentPostUrl, (err, mostRecentPost) ->
+
+ if (err)
+ # duplicate of code handling error
+ else
+
+ # do some stuff for mostRecentPost
+ http.get(mostRecentPost.commentsUrl, (err, comments) ->
+
+ if (err)
+ # yet another duplicate of code handling error
+ else
+
+ # do some stuff for the comments
+```
+
+The styleistic and maintenance problems with this solution are evident, particularly when you imagine more detailed structures of depending actions. First, the rightmost drift makes this code increasingly unreadable and unmaintainable. Second, the error handlers will be repeated with each and every call, even though at most one error handler will ever be executed. Indeed, this problem gets even worse when separate callbacks and "errbacks" are used.
+
+Promises provides an elegant solution to this, because .then returns a promise based upon the returned value of the callback, and hence subsequent .thens are chainable:
+
+```coffeescript
+promiseGet = Promise.fromNode(http.get)
+
+promiseGet('/users/Jim')
+.then((jim)->
+
+ #do some stuff with jim
+ promiseGet(jim.mostRecntPostUrl))
+
+.then((mostRecentPost)->
+
+ #do some stuff with the mostRecentPost
+ promiseGet(mostRecentPost.commentsUrl))
+
+.then((comments)->
+
+ #do some stuff with the comments
+ )
+
+.fail (reason) ->
+
+ # handle the first error based on reason
+```
+
+And using aggregation functions
+
+```coffeescript
+promiseGet = Promise.fromNode(http.get)
+
+promiseGet('/users/Jim')
+.then((jim)->
+
+ # do some stuff with jim
+ Promise.all(jim,
+ promiseGet(jim.mostRecntPostUrl))
+
+.then(([jim, mostRecentPost])->
+
+ # do some stuff with jim AND mostRecentPost
+ Promise.all(jim, mostRecentPost,
+ promiseGet(mostRecentPost.commentsUrl))
+
+.then(([jim, mostRecentPost, comments])->
+
+ #do some stuff with jim, mostRecentPost and comments
+ )
+
+.fail (reason) ->
+
+ # handle the first error based on reason
+
+```
View
@@ -4,7 +4,8 @@
# Covenant
-Covenant is a fully compliant [Promises/A+](https://github.com/promises-aplus/promises-spec) implementation written in Coffeescript. Covenant, its core class is a bare-bones implementation that passes the [Promises/A+ Test Suite](https://github.com/promises-aplus/promises-tests). Covenant is extremely performant and lightweight, its three-function core being 52 lines of Coffeesript, compiling to 170 lines of javascript that minimizes to just 960 bytes uglified and compressed. Promise, more full-featured extension of Covenant is included, weighing in at an additional 62 lines of Coffeescript. Altogether with the Core, Promise compiles to 324 lines and 1.6K bytes uglified and compressed.
+Covenant is a fully compliant [Promises/A+](https://github.com/promises-aplus/promises-spec) implementation written in Coffeescript. Covenant, its core class is a bare-bones implementation that passes the [Promises/A+ Test Suite](https://github.com/promises-aplus/promises-tests). Covenant is performant and extremely lightweight, its three-function core being 52 lines of Coffeesript, compiling to 170 lines of javascript that minimizes to just 960 bytes uglified and compressed. The elegant three-function API (counting the constructor) provides enough functionality to satisfy the Promises/A+ specificationand provide the core for a full-featured promise implementation, which is also providede
+
## The Covenant (Core) API
@@ -26,31 +27,95 @@ p.reject(reason)
covenant.then onFulfilled, onRejected
```
-## Discussion
-
-A promise represents a `value`, that may exist now or in the future, or the `reason` why a value could not be computed. At any point in time, a promise will be either: (i) `pending` resolution; (ii) `fulfilled` with a `value`; or (iii) `rejected` with a `reason`. A pending object can be resolved with the `p.fulfill` and `p.reject` functions. Once resolved, any further call to either function is ignored. Resolution is not guaranteed, and a promise can remaining forever pending.
+## The Promise (Extended) API
-A program performing an asynchronous computation may deliver its result by creating apromise and returning it to the client. The program then manages its state by fulfilling it with a value or rejecting it with a reason as may be required. For example, a promise for delivery of file contents might be built as follows:
+Promise, more full-featured extension of Covenant is included. It weighs in at an additional 62 lines of Coffeescript. Altogether with the Core, Promise compiles to 324 lines and 1.6K bytes uglified and compressed. It provides: a nice collection of promise-generating, an aggregation function, some convenience functions and functions for securely sharing promise objects with clients for limited use.
+
+### Promise Generaton Functions
```coffeescript
-createReadFilePromise = (filename, encoding='utf8') ->
- p = new Covenant
- fs.readFile filename, encoding, (err, value) ->
- if err
- p.reject(err)
- else
- p.fulfill(value)
- p
+{Promise} = require ('covenant')
+
+# Promise.makePromise(f): new up a promise p, apply f(p) and return p
+Promise.makePromise f
+
+# Promise.pending(): construct a pending promise
+p = Promise.pending()
+ .anything(console.log) # => nothing yet!
+ .fulfill("I'm all done") # => I'm all done"
+
+# Promise.fulfilled(value): construct a promise fulfilled with value
+Promise.fulfilled(42)
+ .done(console.log) # => 43
+
+# Promise.rejected(reason): construct a promise rejected for reason
+Promise.rejected("naughty you")
+ .fail(console.error) # => "naughty you"
+
+# Promise.fromNode(nodeOperation): construct a promise generating function based on node functions
+f = Promise.fromNode(fs.readFile)
+pReadFile = f('foo.data')
+
+# construct a promise that fulfills after ms milliseconds
+Promise.delay(100)
+
+# construct a promise that rejects for timeout unless resolved before ms milliseconds.
+Promise.timeout(p, 100)
```
-This pattern is common for node-style callback functions. Once the promise is built, a client receiving the promise can query it by registering resolution handlers. The client may register as many handlers as the programmer likes, both before and after resolution. For example,
+### Aggregate Promise Functions
+```coffeescript
+# Promise.when(promiseOrValueList): Construct a promise from any number of values or promises, which fulfills with an
+# array of corresponding values if all promises are fulfilled, and rejects if ANY
+
+# example when promise is rejected
+# with raw values
+Promise.when(1, 2, 3)
+ .done console.log # => [1, 2, 3]
+
+# with pending promises, ultimately fulfilled
+p = Promise.pending()
+q = Promise.when p, 2, Promise.fulfilled(3)
+q.done console.log # => nothing happens
+p.fulfill(1) # => [1,2,3] after a tick or two
+
+# with pending promises, one rejectedl
+Promise.when(Promise.pending(), Promise.fulfilled(2), Promise.rejected("Error in 3")
+ .fail(console.error) # => Error in 3
+p = (Promise.when p1, p2, p3).fail(console.error) # => Error in 3
+
+# Promise.all(valueOrPromiseList): same as Promise.all Promise.when
+```
+### Promise Instance Convenience Functions
```coffeescript
-createReadFilePromise('filename.txt').
- then console.log, console.error
+# p.done(callback): convenience function for p.then onFulfill, undefined
+p.done(onFulfill)
+
+# p.fail(callback): convenience function for p.then undefined, onReject
+p.fail(onReject)
+
+# p.always(callback): convenience function for p.then callback, callback
+p.always(callback)
+
+# Note that the promise returned by p.always(callback) can resolve
+# differently, even when p has already resolved.
```
-which will log the result value to stdout upon fulfillment, or writes the error to stderr upon rejection.
+### Protected Promise Instance Functions
+```coffeescript
+# p.resolver(): generates an object that can only call
+# reject and fulfill, operating on p
+p.resolver().fulfill(10) # resolves p with value 10
+p.then # => message does not exist
+
+# p.thenable(): generates an object linked to p that responds
+# to then and convenience functions, but does not permit a client to
+# resolve p.
+p.thenable().then console.log
+p.fulfill('hello, world!') # => 'hello, world!'
+p.thenable().fulfill(1) # => message does not exist
+```
## Installation
View
@@ -35,7 +35,7 @@ class CompletedState
p2.reject e
_handleFunctionResult: (datum, callback, fallback, p2) ->
if @_isPromise result=callback(datum)
- secondBestTick => result.then p2.fulfill, p2.reject
+ setImmediate => result.then p2.fulfill, p2.reject
else
p2.fulfill result
_isFunction: (thing)-> typeof thing is 'function'
Oops, something went wrong.

0 comments on commit fb8d301

Please sign in to comment.