Skip to content

Instantly share code, notes, and snippets.

@jcoglan
Last active December 15, 2015 16:48
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jcoglan/5291440 to your computer and use it in GitHub Desktop.
Save jcoglan/5291440 to your computer and use it in GitHub Desktop.
// 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.
@Gozala
Copy link

Gozala commented Apr 2, 2013

@jcoglan or it can be as simple as just decorating sync functions: https://gist.github.com/Gozala/5292787

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