WinJS: Unpacking Promises

N.B. If you don’t know anything about WinJS, take a moment to peruse this primer. Also, the context of this post is the p&p Hilo project. Derick Bailey also wrote about promises on his blog.

A Bit About Promises

Promises are an abstraction for asynchronous programming. If you don’t know anything about them, I recommend that you first read asynchronous programming in JavaScript before you continue.

A promise is an object. It is not a function and it is not the value returned from the async operation. To get to the value, you need to call the then method on the promise object. You pass a callback function as an argument to then. The promise invokes the callback and passes the value you’re interested in into the callback. Clear as mud, right?

Here’s a fictitious example that pretends like calculating a random number requires an async operation:

getRandomNumberAsync().then(function(someNumber) { 
    // do stuff with `someNumber`
});

The call to then returns a promise itself. You could do this:

var afterRandomNumber = getRandomNumberAsync().then(function(someNumber) { 
    // do stuff with `someNumber`
});

afterRandomNumber.then(function() {
    // more stuff
});

Or written another way:

getRandomNumberAsync().then(function(someNumber) { 
    // do stuff with `someNumber`
}).then(function() {
    // more stuff
});

The two example above are the same.

Now if our callback function returns a value, that value is passed along to the next promise’s callback.

getRandomNumberAsync().then(function(someNumber) { 
    return someNumber + 1;
}).then(function(someNumberPlusOne) {

});

This allows you to easily chain promises, piping the output of one into the next callback in the chain.

getRandomNumberAsync().then(function(someNumber) { 
    return someNumber + 1;
}).then(function(someNumberPlusOne) {
    return someNumberPlusOne + 1;
}).then(function(someNumberPlusTwo) {

});

Of course, this is a bit silly when then operations are not async. It’s more interesting when the thing you return from the callback is also a promise. Let’s make a another fictitious async function, this time one that needs input:

getRandomNumberHigherThanAsync(10).then(function(someNumberOverTen){
    // do something with `someNumberOverTen`
});

Now we can do this:

getRandomNumberAsync().then(function(someNumber) { 
    return getRandomNumberHigherThanAsync(someNumber);
}).then(function(something){
    // What will `something` be?
});

In the example above, you might think that something will be the promise returned from getRandomNumberHigherThanAsync. It’s not. Instead, it’s the value that getRandomNumberHigherThanAsync produces and would pass into its callback. Returning another promise from within the callback for a promise is a special case. Though it’s probably the most frequent case.

Putting Promises Together

Now let’s pretend we have a set of functions that all return promises, named A through E. If we wanted to execute them in sequence, passing the results from one to the next, we could write it this:

A().then(function(a) {
    return B(a).then(function(b){
        return C(b).then(function(c){
            return D(c).then(function(d){
                return E(d);
            });
        });
    });
});

Yeah, that hurts my eyes too. Though I found that I was writing my code just like this at first.

However, we should realize that A.then() returns a promise and that that promise completes only when all of the nested promises have completed. If we wanted to execute a new function F after all these steps, we could do it like this:

var waitForAllToBeDone = A().then(function(a) {
    return B(a).then(function(b){
        return C(b).then(function(c){
            return D(c).then(function(d){
                return E(d);
            });
        });
    });
});

waitForAllToBeDone().then(function(e){
    return F(e);
});

However, that last inline callback has the same signature as F. That means that we can simplify to this:

waitForAllToBeDone().then(F);

Now we realize that what we did for F is also true for E. In fact, it is true for the entire chain. We can simplify that nasty nested beast to:

A().then(B).then(C).then(D).then(E).then(F);

Much nicer.

A Real Example

Let’s bring this home. While working on HiloJS we needed to copy an image thumbnail to a new file. It sounds simple, but it requires the following steps:

  1. Open a file that we will write to. We’ll call this the target file.
  2. Get the thumbnail image from another file. We’ll call this the source file. (WinRT creates the thumbnail for us from the source.)
  3. Copy the stream from the thumbnail source to the target file’s input stream.
  4. Flush the output stream.
  5. Close both the input and the output stream.

(Actually we don’t really care about the order of the first two steps. They could be switched.)

Our initial implementation looked liked this:

function writeThumbnailToFile(sourceFile, targetFile) {

    var whenFileIsOpen = targetFile.openAsync(fileAccessMode.readWrite);

    return whenFileIsOpen.then(function (outputStream) {

        return sourceFile.getThumbnailAsync(thumbnailMode.singleItem)).then(function (thumbnail) {
            var inputStream = thumbnail.getInputStreamAt(0);
            return randomAccessStream.copyAsync(inputStream, outputStream).then(function () {
                return outputStream.flushAsync().then(function () {
                    inputStream.close();
                    outputStream.close();
                });
            });
        });
    });
}

Then we had a code review with the always helpful Chris Tavares. He pointed us in a more excellent direction. We were able to change the code to this:

function writeThumbnailToFile(sourceFile, targetFile) {

    var whenFileIsOpen = targetFile.openAsync(fileAccessMode.readWrite);
    var whenThumbailIsReady = sourceFile.getThumbnailAsync(thumbnailMode.singleItem);

    var whenEverythingIsReady = WinJS.Promise.join([whenFileIsOpen, whenThumbailIsReady]);

    var inputStream, outputStream;

    whenEverythingIsReady.then(function (args) {
        outputStream = args[0];
        var thumbnail = args[1];
        inputStream = thumbnail.getInputStreamAt(0);
        return randomAccessStream.copyAsync(inputStream, outputStream);

    }).then(function () {
        return outputStream.flushAsync();

    }).then(function () {
        inputStream.close();
        outputStream.close();
    });
}

A couple of notable differences:

  1. In the first implementation, we passed along some values via the closure (e.g., inputStream and outputStream). In the second, we had to declare them in the outer scope because there was no common closure.

  2. In the first implementation, we chained targetFile.openAsync and sourceFile.getThumbnailAsync, but we didn’t really need to. We made the real relationship more explicit in the second using WinJS.Promise.join. That mean the values of these two promises came to us in an arrays (we named it args).

Summary

Understanding how promises can be composed really helped us to make the code more readable. It can be difficult to wrap your head around the way they work, but (like it or not) promises are an essential part of writing apps with WinJS.

Fictitious Functions Implementations

// an example implementation of getRandomNumberAsync

function getRandomNumberAsync() {
    return WinJS.Promise.as(Math.random());
}

// an example implementation of getRandomNumberHigherThanAsync

function getRandomNumberHigherThanAsync(minimum) {
    var someNumber = Math.random() + minimum;
    return WinJS.Promise.as(someNumber);
}
Comment on this post at dev.bennage.com

Posted 08-22-2012 2:15 AM by Christopher Bennage

[Advertisement]

About The CodeBetter.Com Blog Network
CodeBetter.Com FAQ

Our Mission

Advertisers should contact Brendan

Subscribe
Google Reader or Homepage

del.icio.us CodeBetter.com Latest Items
Add to My Yahoo!
Subscribe with Bloglines
Subscribe in NewsGator Online
Subscribe with myFeedster
Add to My AOL
Furl CodeBetter.com Latest Items
Subscribe in Rojo

Member Projects
DimeCasts.Net - Derik Whittaker

Friends of Devlicio.us
Red-Gate Tools For SQL and .NET

NDepend

SlickEdit
 
SmartInspect .NET Logging
NGEDIT: ViEmu and Codekana
LiteAccounting.Com
DevExpress
Fixx
NHibernate Profiler
Unfuddle
Balsamiq Mockups
Scrumy
JetBrains - ReSharper
Umbraco
NServiceBus
RavenDb
Web Sequence Diagrams
Ducksboard<-- NEW Friend!

 



Site Copyright © 2007 CodeBetter.Com
Content Copyright Individual Bloggers

 

Community Server (Commercial Edition)