14 ms·
Callbacks are imperative, promises are functional
- tomdale 14y agoJames does a good job of articulating why promises are such a useful abstraction, especially in JavaScript land. I've been working on a project recently that relies heavily on coordinating many asynchronously-populated values, and I don't even want to think about what the code would look like if we were wrangling callbacks manually. We actually extracted our promises implementation from the work we've been doing, and released it as RSVP.js[1]. While other JavaScript promises libraries are great, we specifically designed RSVP.js to be a lightweight primitive that can be embedded and used by other libraries. Effectively, it implements only what's needed to pass the Promises/A+ spec[2]. For a comparison of RSVP.js with other promises-based JavaScript asynchrony libraries, see this previous discussion on Hacker News[3]. 1: https://github.com/tildeio/rsvp.js https://github.com/tildeio/rsvp.js 2: https://github.com/promises-aplus/promises-spec https://github.com/promises-aplus/promises-spec 3: https://news.ycombinator.com/item?id=4661620 https://news.ycombinator.com/item?id=4661620
- steveklabnik 14y ago> If foo takes many arguments we add more arrows, i.e. foo :: a -> b -> c > means that foo takes two arguments of types a and b and returns something of > type c. Nitpick alert: since everything is curried in Haskell, it's actually more like `foo takes an argument a and returns a function that takes one b and returns one c`. Other than that teeny thing, this article is awesome, and I fully agree. Promises are an excellent thing, and while I'm just getting going with large amounts of JavaScript, they seem far superior to me.
- threedaymonk 14y ago> Nitpick alert: since everything is curried in Haskell, it's actually more like `foo takes an argument a and returns a function that takes one b and returns one c`. Whilst that's true, and is important to the way in which Haskell operates, people normally talk about functions as taking multiple arguments (at least, the people at London HUG, most of whom are better Haskellers than I, seem to). Even ghci refers to the "second argument": Couldn't match expected type `Int' with actual type `Char' In the second argument of `foo', namely 'b' In the expression: foo 1 'b' In an equation for `it': it = foo 1 'b'
- steveklabnik 14y agoOf course, hence the 'nitpick alert' and admission that it doesn't really affect anything in the text, just a detail about how things work. Often, conversations are not held to absolute rigor. Not every off-handed statement is absolutely consistent.
- andrus 14y agoReally? Consider f :: a -> b -> a f a = g a g :: a -> b -> a g a _ = a It doesn't seem right to say that g "returns a function that takes one b", whereas you could say that about f.
- steveklabnik 14y agoYes. http://www.haskell.org/haskellwiki/Currying http://www.haskell.org/haskellwiki/Currying
- andrus 14y agoThank you for clarifying! I did not know that all functions in Haskell are considered curried. My surprise stemmed in part from reading a bit about "arity" from [1]. It's interesting how the theoretical model of Haskell--"all functions in Haskell take just single arguments"--differs from implementation, where, for functions of known arity, GHC in particular does not actually "follow the currying story literally" [2]. [1] http://hackage.haskell.org/trac/ghc/wiki/Commentary/Rts/HaskellExecution/FunctionCalls#Genericapply http://hackage.haskell.org/trac/ghc/wiki/Commentary/Rts/Hask... [2] http://community.haskell.org/~simonmar/papers/eval-apply.pdf http://community.haskell.org/~simonmar/papers/eval-apply.pdf
- steveklabnik 14y agoAny time. It's one of the more interesting parts of Haskell to me, so it's one I always remember. You're absolutely right to point out that implementations and theory often differ; compilers often do tricky things behind the scences.
- Evbn 14y ago(g a) is valid Haskell and it is equal to a constant function that returns a. In fact, g is the Prelude function 'const'.
- niggler 14y ago" the decision, made quite early in its life, to prefer callback-based APIs to promise-based ones." Rewind to the point when nodejs was being designed. In that world, in the context of javascript, callbacks were the only real pattern that existed. XHR? callback. Doing something in the future? callback. If you imagine node trying to leverage the javascript ecosystem, callbacks were a no-brainer.
- lucian1900 14y agoVarious incarnations of the Promise monad have existed for quite a while, even in JS. The oldest one I can think of is MochiKit's Deferred, inspired by Twisted's. That one worked (and still does) seamlessly with any callback code.
- abecedarius 14y ago(Twisted was inspired by the work I just pointed to in my answer. Not to take away from yours -- I wasn't familiar with MochiKit.)
- lucian1900 14y agoOf course, using monads for asynchronous tasks is an old trick and E has always been ahead of its time (like many other languages ...)
- abecedarius 14y agoMaybe so, but everything in this article goes back at least to the 90s with the E programming language (http://erights.org http://erights.org). Doug Crockford was involved in E. (Nowadays E's Mark Miller is on the Ecmascript committee.)
- kevingadd 14y agoI'm not sure you've ever used XHR if you call it the callback pattern. The XHR object is effectively a request and a response bundled up into one object that has promise-like traits. You attach event handlers to it to handle various state changes and scenarios, and then once you issue the request, the event handlers get invoked 0-N times. If it really was JavaScript callback-style, XHR would look like this: window.xmlHttpRequest("GET", url, function (result, error) { ... } ); It doesn't. setTimeout/setInterval are definitely callback-passing, but they're not exactly glowing examples of stellar API design. They return integer IDs instead of handles or objects! Honestly, the only way to classify node's callback-heavy design as a 'no-brainer' is if you excuse the design by saying no thought was put into it beyond simply doing what a bunch of other people were doing. If you put enough thought into how large applications will be built, and how difficult it is to build scalable, maintainable libraries, callback-passing style easily loses compared to promises.
- jQueryIsAwesome 14y agoAll the code and such a big abstraction for the first example when it could have done like this: var result = []; paths.forEach(function (i, file){ fs.stat(file, function (err, data){ result.push(data); if (i === 0) { // Use stat size } if (result.length === paths.length) { // Use the stats } }); }); Fairly understandable, more efficient and without introducing logic patterns foreign to many. It also meet his requirements (It is parallel and we only hit every file once)
- deleted 14y ago[deleted]
- kevingadd 14y agoThe real problem, design-wise, is that fs.stat operates on a single file at a time. Sometimes you only want info on one file, sure, but in many common use cases, you want info on a bunch of files - perhaps even the contents of an entire directory, or a directory tree. Worse still, stat might be a syscall! Woo, syscall per file.
- jQueryIsAwesome 14y ago... and what does that haves to do with promises? And in such case you would only do this once outside the listeners of http/or-whatever connections so it would be done just once regardless of the number of concurrent activity.
- kevingadd 14y agoThe point is that a properly designed API wouldn't require any amount of scaffolding. You'd go: fs.statMany(filenames, function (stats) { ... }); or: var statsPromise = fs.statMany(filenames); And then in either case, you'd just use a for-loop or forEach or whatever your preference on the result. No thinking about how to preserve complex invariants or whatever is necessary. Hell, with ES6 generator-y expressions you could make it even more succinct, something like: var result; fs.statMany(filenames, function (stats) { result = [... for x in stats]; }); No push nonsense, no nested if statements, no need to explicitly invoke async.parallel or whatever. Just clarity.
- crazygringo 14y agoThis is an interesting perspective. But to me, even having spent a year on a large node.js project, I just don't see how promises would have simplified things at all. If you have some crazy graph of dependencies, I can see how breaking out promises could help simplify things. But I don't feel like that's a super-common scenario. The author says: > * [Promises] are easier to think about precisely because we’ve delegated part of our thought process to the machine. When using the async module, our thought process is:* > A. The tasks in this program depend on each other like so, > B. Therefore the operations must be ordered like so, > C. Therefore let’s write code to express B. > Using graphs of dependent promises lets you skip step B altogether. But in most cases, I don't want to skip B. As a programmer, I generally find myself preferring to know what order things are happening in. At most, I'll parallelize a few of database calls or RPC's, but it's never that complex. (And normal async-helper libraries work just fine.) I swear I want to wrap my head around how this promises stuff could be useful in everyday, "normal" webserver programming, but it just always feels like over-abstraction to me, obfuscating what the code is actually doing, hindering more than helping. I want to know, specifically, if one query is running before another, or after another, or in parallel -- web programming is almost entirely about side effects, at least in my experience, so these things often matter an awful lot. I'm still waiting for a real-world example of where promises help with the kind of everyday webserver (or client) programming which the vast majority of programmers actually do. > Getting the result out of a callback- or event-based function basically means “being in the right place at the right time”. If you bind your event listener after the result event has been fired, or you don’t have code in the right place in a callback, then tough luck, you missed the result. This sort of thing plagues people writing HTTP servers in Node. If you don’t get your control flow right, your program breaks. I have literally never had this problem. I don't think it really plagues people writing HTTP servers. I mean, you really don't know what you're doing if you try to bind your event listener after a callback has fired. Remember, callbacks only ever fire AFTER your current imperative code has finished executing, and you've returned control to node.js.
- TheZenPsycho 14y agoThe point is promises free you from wanting or needing to know about the order that things happen in. I hear you saying you fear promises, because it means it would get in the way of your ability to know that. But the truth is once you embrace them, that need becomes unimportant. The idea that webservers are "all about side effects" gives me a chill. The whole architecture concept of HTTP is no side effects, so to claim that it's all about side effects seems odd. It should only be the case for POST PUT or DELETE methods, and only in very specific ways.
- ww520 14y agoI feel this is twisting the meaning of functional programming. Excel is not functional. It is declarative. You declare the relationships between the cells and Excel uses those to propagate changes. Just like a makefile is not functional but declarative. The dependency of the relationships are enforced to produce action. SQL is another example of declarative language and it is nowhere near as functional.
- hippobravo 14y agoThis was my thought as well. Promises are declarative... making a promise is almost the very definition of declarative programming. It's not functional at all. This reaffirms my belief that blog posts are a terrible place to learn. People who know the least shout the loudest.
- jasondenizac 14y agoHN comment threads are my favorite learning environment.
- jewbacca 14y agoA tyrannical dichotomy. Functional programming is declarative. Especially when it's lazy and the program's instantaneous state is abstracted out. One of the motivating goals in functional programming is to be able to define a computation once, in terms of other computations, and have that relationship be maintained with minimal regard to the state of the program or its order of execution. Which is what it seems like (this is the first time I've specifically encountered them) Promises are a powerful tool for accomplishing. In contrast, threading explicit callbacks/continuations through a program, which is explicitly managing the order of execution, is relatively more imperative, which I think is the point of the article -- you don't need the wider control-flow flexibility of explicitly and manually threading callbacks to do the type of computations that most async web stuff does. You can abstract the common callback pattern out into something like Promises, and make all your shit more consistent and concise.
- hippobravo 14y ago
- deleted 14y ago[deleted]
- TheZenPsycho 14y agoThis is a ridiculous attitude. Do you laugh at the concept of "complex" and "imaginary" numbers because a group of these so called "mathematicians" have apparently arbitrarily decided to call some numbers imaginary? RIDICULOUS! LAUGHABLE!
- deleted 14y ago[deleted]
- arianvanp 14y agoThis kind of programming certainly is promising </pun> It's one of the reasons why i started learning haskell.
- mbostock 14y agoNot to focus too myopically on the given example, but I can’t help but wonder why it’s a requirement that the first file be handled specially? A less contrived example would make the argument more convincing. If I wanted to compute the size of one file relative to a set, I’d probably do something like this: queue() .defer(fs.stat, "file1.txt") .defer(fs.stat, "file2.txt") .defer(fs.stat, "file3.txt") .awaitAll(function(error, stats) { if (error) throw error; console.log(stats[0].size / stats.reduce(function(p, v) { return p + v.size; }, 0)); }); Or, if you prefer a list: var q = queue(); files.forEach(function(f) { q.defer(fs.stat, f); }); q.awaitAll(…); // as before This uses my (shameless plug) queue-async module, 419 bytes minified and gzipped: https://github.com/mbostock/queue https://github.com/mbostock/queue A related question is whether you actually want to parallelize access to the file system. Stat'ing might be okay, but reading files in parallel would presumably be slower since you'd be jumping around on disk. (Although, with SSDs, YMMV.) A nice aspect of queue-async is that you can specify the parallelism in the queue constructor, so if you only want one task at a time, it’s as simple as queue(1) rather than queue(). This is not a data dependency, but an optimization based on the characteristics of the underlying system. Anyway, I actually like promises in theory. I just feel like they might be a bit heavy-weight and a lot of API surface area to solve this particular problem. (For that matter, I created queue-async because I wanted something even more minimal than Caolan’s async, and to avoid code transpilation as with Tame.) Callbacks are surely the minimalist solution for serialized asynchronous tasks, and for managing parallelization, I like being able to exercise my preference.
- DougBTX 14y agoIt looks like you're able to return one of those queues from a function and allow some other code to call .await(). Being able to return something is a useful feature of promises too, seems like there might be more overlap there.
- jasondenizac 14y ago> I just feel like they might be a bit heavy-weight and a lot of API surface area to solve this particular problem. The surface area is `.then()`
- graue 14y agoThis code doesn't look right to me: // list :: [Promise a] -> Promise [a] var list = function(promises) { var listPromise = new Promise(); for (var k in listPromise) promises[k] = listPromise[k]; Perhaps the assignment is supposed to be the other way around? for (var k in promises) listPromise[k] = promises[k];
- aethertap 14y agoI agree, I was going to ask the same question. Unless there's some subtlety that I'm missing, the order you propose makes much more sense.
- ams6110 14y agoI'm glad someone else thought that looked odd.
- pyrtsa 14y agoI asked the same question in Twitter. Turns out James was actually augmenting (i.e. modifying) the array object `promises` to behave as a promise itself. I don't think this was a particularly beautiful way of doing it but it seems to work now that I think of it. Promise libraries, like RSVP.js [1] he referred to, typically implement a way to construct a promise with a depends-on-many relationship, as a function possibly called `all([p1, p2, ...])` (with the same type signature as for `list`), `and(p1, p2, ...)` or something similar. IMO, defining the `list` function that way would've been clearer to the reader and more FP'ish, treating the `promises` argument in as a value and not a mutable object. [1]: https://github.com/tildeio/rsvp.js/blob/master/lib/rsvp/all.js https://github.com/tildeio/rsvp.js/blob/master/lib/rsvp/all....
- graue 13y agoA day later I looked at this again and I'm a little closer to understanding. var listPromise = new Promise(); creates an object that, being a Promise object, has certain methods and internal state, derived from the prototype of Promise. for (var k in listPromise) promises[k] = listPromise[k]; This confused me because I thought "k" was a stand-in for a numeric index, e.g. that it was doing promises[0] = listPromise[0], promises[1] = listPromise[1], etc. That is not what's going on. Rather, "k" refers to attributes and/or methods that objects of the Promise class have by default. It's copying those onto `promises` — the array `promises` itself, not the individual items `promises[i]`, which keep their existing methods and attributes. Coming from a Python background, I think I would have found this more obvious if the variable "k" were instead called "method" or "attr". If it was `for (var method in listPromise)` it'd be much clearer what's going on, whereas single-letter variables like i, j, and k are, to me, stand-ins for integers. It was also confusing, as you said, that the function uses destructive update rather than treating the input as a value. James did mention this ("augmenting the list with promise methods"), but it's still unexpected, especially when the function is preceded by a Haskell type signature. The reason I only say I'm closer to understanding, and not quite there yet, is I'm not sure what it means to do `new Promise()` or what is being copied over in the above for-loop. I tried James's code with a Promises/A+ implementation, rsvp.js (https://github.com/tildeio/rsvp.js https://github.com/tildeio/rsvp.js), but it won't let me do `new Promise()` because it works differently: > var promise = new RSVP.Promise(); TypeError: You must pass a resolver function as the sole argument to the promise constructor Per an example in RSVP.js's readme, it's expecting this: var promise = new RSVP.Promise(function(resolve, reject){ // set up a callback that calls either resolve(...) // or reject(...) }); If James is using a specific promises implementation in his code, it appears to be the one he defined in a past blog post (http://blog.jcoglan.com/2011/03/11/promises-are-the-monad-of-asynchronous-programming/ http://blog.jcoglan.com/2011/03/11/promises-are-the-monad-of...), which in turn builds on a module from his JS.Class library (http://jsclass.jcoglan.com/deferrable.html http://jsclass.jcoglan.com/deferrable.html), which I hadn't heard of before. I still think this is a great article, but that code snippet has proven to be quite a puzzle.
- richo 14y agoSo, calling magic subroutines is more functional than passing about first class functions?
- naradaellis 14y agoFunctional programming is not just about first class functions and I disagree with the idea that using first class functions means you are doing "functional programming". See the second paragraph of the article for the authors take on this. Specifically, the concept of values is very important.
- spullara 14y agoIt is hard for me to fathom the negative feelings towards Promises. They are quite clearly a great way to perform async programming in a civilized way (see Twitter's Future/Promise in Finagle on github). JDK 8 will even have the equivalent in CompletableFuture. The only thing better is to combine Promises with coroutines for a more linear programming style like in Flow: http://www.foundationdb.com/white-papers/flow/ http://www.foundationdb.com/white-papers/flow/
- markov_twain 14y agoThanks for this--I mentioned finagle to jcoglan on twitter yesterday after I read his blog post, and I don't think he was aware of the similarities. I actually didn't know about futures until I learned about scala and finagle. I watched a talk on twitter's service stack given by marius eriksen and was blown away. My coworkers heard me rambling on about futures for weeks afterwards, and I found that it was difficult to explain what was so great about them. So I'm not surprised at the negative reactions in the comments here (although jcoglan did a much better job of exlaining them than I ever did).
- dschobel 14y agoif you want this on the JVM today and can abide Scala, see: http://doc.akka.io/docs/akka/snapshot/scala/dataflow.html http://doc.akka.io/docs/akka/snapshot/scala/dataflow.html
- spullara 14y agoThe Twitter solution I mention above is in Scala — that said, I have one that also works in JDK 6/7 in a branch of https://github.com/spullara/java-future-jdk8 https://github.com/spullara/java-future-jdk8.
- ricardobeat 14y agoRyan Dahl in February 2010, when Promises were removed from core: Because many people (myself included) only want a low-level interface to file system operations that does not necessitate creating an object, while many other people want something like promises but different in one way or another. So instead of promises we'll use last argument callbacks and consign the task of building better abstraction layers to user libraries. Those libraries do exist. There still isn't a canonical Promises specification. Node trying to force promises onto the ecosystem early on would've been like applying brakes and slow down adoption enormously.
- mjackson 14y ago> There still isn't a canonical Promises specification. Yes, there is: https://github.com/promises-aplus/promises-spec https://github.com/promises-aplus/promises-spec
- ricardobeat 14y agoPromises/A+ surfaced less than 6 months ago, and is not implemented by most widely-used frameworks. Still a bit far from canonical.
- tlrobinson 13y agoPromises/A+ is just a more fully specified version of Promises/A, which has been around for about 4 years.
- just2n 14y agoPromises are just tools for managing a list of callbacks with less boilerplate. I wouldn't call one imperative and the other functional. Both are functional. You might dislike callback patterns, but through one of the beautiful parts of JS, you can trivially wrap any callback-oriented API you want and have it become a promise based one. I've done this before when I had a very complex dependency graph at the start of a program and a few API calls were callback related. It looks something like this: SomeClass.prototype.someActionPromise = function(){ var deferred = makeADeferred(); SomeClass.prototype.someAction.call(this, function(err){ err ? deferred.reject() : deferred.resolve(); }); return deferred.promise(); }; Now you have a promise-based version that makes your code a little cleaner and easier to read.
- naradaellis 14y agoThe author "promisify"s a callback-based API in the article - which is worth a read by the way. I'm interested in your opinion RE: his argument for why callback APIs are imperative - because I think he has a very good point and has supported it with a solid argument and you haven't offered any rebuttal.
- just2n 14y agoInteresting. As you pointed out, I hadn't read the article but was rather replying to other comments. After reading it, I think I have to agree that I had never thought about it that way. It makes a lot more sense that a promise is just a declaration of some unit of work, and when you can use a promise like any other data, you aren't just giving imperative commands, but rather describing work to be done and using that as a fundamental part of your code, which is why it drastically simplifies async programming (the relation of promises to monads is quite nice, too). Definitely a good article, thanks for kicking me :).
- SeanDav 14y agoI don't agree that there is any fundamental difference in functionality between callbacks and promises. Promises don't somehow magically make asynchronous code easy to write while leaving callbacks out in the cold. They have very similar strengths and weaknesses and I didn't find any of the OP's arguments compelling. In fact, if I had to choose, I would take the opposite view and say callbacks are neater, cleaner and more consistent than promises.
- tomp 14y agoPromises are values, and you can use them to compute things. Callbacks are procedures, and are non-composable in non-trivial ways (you can chain callbacks very simply, but that's basically it).
- ilaksh 14y agoPromises seem cool but if you are not liking callbacks very much you should take a look at just using CoffeeScript indenting two spaces, specifying functions instead of inline, he async module, icedcoffeescript with await and defer, and Livescript with backcalls. All of that is more useful and straightforward than promises.
- eldude 14y agoUnfortunately, in practice promises end up making your code more difficult to reason about by adding cruft and unnecessary abstraction. They're also very limiting from a control-flow perspective. This is especially noticeable when you have branching behavior / want to resolve a promise early[1]: Branching with promises: function doTask(task, callback) { return Q.ncall(task.step1, task) .then(function(result1) { if (result1) { return result1; } else { return continueTasks(task); } }) .nodeify(callback) } function continueTasks(task) { return Q.ncall(task.step2, task); .then(function(result2) { return Q.ncall(task.step3, task); }) } As opposed to with stepdown[2]: function doTask(task, callback) { $$([ $$.stepCall(task.step1), function($, result1) { if (result1) return $.end(null, result1) }, $$.stepCall(task.step2), $$.stepCall(task.step3) ], callback) } I would really love for a post to include a non-trivial problem implemented with promises, vanilla callbacks, and async (and I'd be happy to add a stepdown equivalent), and allow people to see for themselves (how in my opinion promises make code harder to read). [1] http://stackoverflow.com/questions/11302271/how-to-properly-abort-a-node-js-promise-chain-using-q http://stackoverflow.com/questions/11302271/how-to-properly-... [2] https://github.com/Schoonology/stepdown https://github.com/Schoonology/stepdown (docs need updating, view tests for documentation)
- pk11 14y agothere is also a third approach for those who want to write composable, functional javascript http://dfellis.github.com/queue-flow/2012/09/21/tutorial/ http://dfellis.github.com/queue-flow/2012/09/21/tutorial/