tweets.js

5 min read Original article ↗
// This is in response to https://gist.github.com/Peeja/5284697. // Peeja wanted to know how to convert some callback-based code to functional // style using promises. var Promise = require('rsvp').Promise; var ids = [1,2,3,4,5,6]; // If this were synchronous, we'd simply write: // getTweet :: ID -> Tweet var getTweet = function(id) { return 'Tweet text ' + id }; // upcase :: Tweet -> String var upcase = function(tweet) { return tweet.toUpperCase() }; var rotten = ids.map(function(id) { return upcase(getTweet(id)); }); // Or, to put it in the form of a pipeline with two map calls: var rotten = ids.map(getTweet).map(upcase); console.log(rotten); // This form is useful because it models the problem as a pipeline that points // the way to a good async solution. When we have synchronous code, the type // signatures are: // // getTweet :: ID -> Tweet // upcase :: Tweet -> String // // If we model this with proimses, we have: // // getTweet :: ID -> Promise Tweet // upcase :: Tweet -> Promise String var getTweet = function(id) { var promise = new Promise(); setTimeout(function() { promise.resolve('Tweet text ' + id) }, 500); return promise; }; var upcase = function(tweet) { var promise = new Promise(); setTimeout(function() { promise.resolve(tweet.toUpperCase()) }, 200); return promise; }; // Let's import our list() helper for managing collections of promises: // list :: [Promise a] -> Promise [a] var list = function(promises) { var listPromise = new Promise(); for (var k in listPromise) promises[k] = listPromise[k]; var results = [], done = 0; promises.forEach(function(promise, i) { promise.then(function(result) { results[i] = result; done += 1; if (done === promises.length) promises.resolve(results); }, function(error) { promises.reject(error); }); }); if (promises.length === 0) promises.resolve(results); return promises; }; // Taking this one step at a time, let's map a list of IDs to a list of // `Promise Tweet`: var tweetPromises = ids.map(getTweet); // We can map this list of `Promise Tweet` to a list of `Promise String`: var rottenPromises = tweetPromises.map(function(promise) { return promise.then(function(tweet) { return upcase(tweet) }); }); // Then we can just join this list to get the eventual results: list(rottenPromises).then(console.log); // However the middle stage of this is kind of messy. In our synchronous code, // we had a function of type (ID -> Tweet) and one of type (Tweet -> String), // and we could compose them. Now we have one of type (ID -> Promise Tweet) // another of type (Tweet -> Promise String). We'd like the whole pipeline to // give us (ID -> String), but we can't feed a `Promise Tweet` into a function // that just takes a `Tweet` -- this is why we need a bunch of glue around the // upcase function to extract the Tweet from the Promise. // // But this glue is generic: it's part of the Promise monad that I cover in // http://blog.jcoglan.com/2011/03/11/promises-are-the-monad-of-asynchronous-programming/ // // We can convert a function of type (a -> Promise b) into a function of type // (Promise a -> Promise b) in a generic way, namely: // bind :: (a -> Promise b) -> (Promise a -> Promise b) var bind = function(fn) { return function(promise) { return promise.then(function(value) { return fn(value) }); }; }; // This lets us rewrite our solution like so: var rotten = ids.map(getTweet).map(bind(upcase)); list(rotten).then(console.log); // We can take this a step further by wrapping the initial list of IDs in a // Promise, using the unit() function: // unit :: a -> Promise a var unit = function(a) { var promise = new Promise(); promise.resolve(a); return promise; }; // Then we get this pipeline: var rotten = ids.map(unit).map(bind(getTweet)).map(bind(upcase)); list(rotten).then(console.log); // Or, we can rewrite with the pipeline acting on each element, separating the // concerns of the promise pipeline from the map operation: var b_getTweet = bind(getTweet), b_upcase = bind(upcase); var rotten = ids.map(function(id) { return b_upcase(b_getTweet(unit(id))); }); list(rotten).then(console.log); // But, the concept of piping a value through a series of functions of type // (a -> Promise b) can be made generic, if we invent our own form of // Haskell's do-notation as I did in: // http://blog.jcoglan.com/2011/03/06/monad-syntax-for-javascript/ // pipe :: a -> [a -> Promise b] -> Promise b var pipe = function(input, functions) { var promise = unit(input); functions.forEach(function(fn) { promise = bind(fn)(promise); }); return promise; }; // Which lets us write the solution as: var rotten = ids.map(function(id) { return pipe(id, [getTweet, upcase]); }); list(rotten).then(console.log); // Or, we can make a function for composing two promise returning functions, // and really clean things up: // compose :: (b -> Promise c) -> (a -> Promise b) -> (a -> Promise c) var compose = function(f, g) { return function(x) { return g(x).then(function(y) { return f(y) }); }; }; var rotten = ids.map(compose(upcase, getTweet)); list(rotten).then(console.log); // This leaves with something fairly expressive that separates concerns cleanly // and is quite easy to change.