Last active
December 15, 2015 16:48
-
-
Save jcoglan/5291440 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@jcoglan or it can be as simple as just decorating sync functions: https://gist.github.com/Gozala/5292787