Make No Promises
swannodette.github.io
swannodette.github.io
Some commenters seem to assume David's point is "use core.async because it's faster than promises". I don't think so. My takeaway from this post is that, having been persuaded core.async is awesome and will give me joy as a programmer (and see David's other posts for evidence of that, e.g. [1]), performance concerns should not prevent me from going for it, because it's competitive.
For me personally, Clojure and ClojureScript still have a long way to go to catch up with JavaScript-based development using Node.js and say, Angular. A smaller community, fewer libraries and less mature tools makes it kind of a bummer from a social perspective. I'm also not impressed with the Clojure/ClojureScript code-sharing story, which requires hacks[2] to use the same code on the client and server. Here, Node frameworks like Derby[3], which emphasizes sharing nearly all your code, and Airbnb's Rendr[4], seem light-years ahead.
But when you consider that this is an early release of a language and async lib written by a surprisingly small team, with basically no corporate backing, it's a pretty amazing accomplishment. I love ClojureScript in theory and I'm eager for the day when I can love it in practice too.
[1] http://swannodette.github.io/2013/07/31/extracting-processes...
I think this is definitely a "the glass is half-empty" interpretation. I mean, let's point out that you're complaining about an environment where you can share code between a codebase running on the server and one on the client--and it isn't JavaScript. And it's not ideal? Of course it's not ideal. But you can do inter-op with Java (server-side) and JavaScript (client-), using the same syntax, and with cljx, you can do this in the same file. This is freaking awesome, even with the messy bits that come along with it, in my opinion.
A smaller community, fewer libraries and less mature tools makes it kind of a bummer from a social perspective.
As far as this point, there's less I can disagree with: the community, while small, is awesome, but there is a way to go before ClojureScript, at least, is going to have the ease of "install and get going" that you have with other platforms. Everything from testing to getting a reasonable console up and running is in a state of flux and cannot be favorably compared to JavaScript, other than the argument that ClojureScript provides a better enough programming experience to outweigh the ease-of-setup that JS provides. For me that's more than enough, but for others I can understand that it's not, yet.
But it absolutely is improving, very quickly. We really need more people to be using CLJS and hyping it (as David Nolen is doing) and putting in the time to shave the yaks that need shaving--as people like David Nolen, Kevin Lynagh (http://keminglabs.com/blog/cljs-app-designs/ as well as the aforementioned cljx), Chas Emerick (many contributions, but recently https://github.com/cemerick/austin), Mimmo Cosenza (the fantastic Modern CLJS series: https://github.com/magomimmo/modern-cljs) and many others are doing--so that we can get to that point where the advantages clearly outweigh the disadvantages. I think that time will come, but it's going to require some serious work.
But even now there are compelling reasons to use ClojureScript, including libs like core.async. I think you'd find that, with a bit of patience, there is a lot there to keep you invested in the tools and the community.
EDIT: I just want to add that cljx is not the only or oldest or simplest way to share code. Crossovers using lein-cljsbuild have provided this for a while: https://github.com/emezeske/lein-cljsbuild/blob/master/doc/C...
I did a little project in CLJS and found the additional power of the language nice, but not revolutionary. I've found getting into Node.js that despite the well-known warts of JavaScript, the community has done an amazing job working around those problems, building powerful abstractions, and shipping simple, un-complected code. I hardly feel like I'm compromising on expressivity.
So for me, language like "callback hell" and "wasteful" rings a bit hollow, as it doesn't reflect my experience writing JavaScript. In fact, if I were a contributor to When.js I could imagine it being pretty insulting.
Maybe we can win more people to ClojureScript by recognizing, not diminishing, the great stuff folks have made with vanilla JS, and showing how CLJS provides interop while making JavaScript programming even better. Kevin Lynagh's post here[1] is a nice example of that.
[1] http://keminglabs.com/blog/angular-cljs-weather-app/
As for my "glass is half-empty" interpretation of code-sharing, I'm thinking of how you can trivially require jQuery from a Node.js application and use it for scraping[2]. Or how you can require('http') in the browser and effortlessly make HTTP requests in browser or server with the same code[3]. I'd love to be proven wrong, but ClojureScript seems far away from this kind of stuff. You can't use Compojure or HTTP-Kit in ClojureScript. So you could say that while Clojure has a decent client/server code-sharing story, there's this other thing, JavaScript, that has an amazing one.
[2] http://blog.nodejitsu.com/jsdom-jquery-in-5-lines-on-nodejs
The promise pattern is not meant to speed up callback-based code, or to eliminate callbacks altogether.
The purpose of a promise is to allow an asynchronous operation to be cancelled. In the old way, there'd be no standard way to cancel something like this,
xhr.get(url, function (data) {
// do something with the data
});
If we have a promise, we can cancel it, getPromise = xhr.get(...);
// later...
getPromise.cancel();
By creating a standard promise pattern, you can have any number of heterogeneous asynchronous operations chained or canceled together uniformly.I imagine an advocate of core.async would argue that they have a good analog for canceled promises, though, namely closing channels. On the other hand, closed channels will still yield nil when read from; the procedures trying to read from a closed channel would need to check for that.
I may be misunderstanding your point, but you could name the callback function, and give the function object itself a "cancel()" method which alters its behavior to do nothing.
It's not, and the javascript Promise/A spec has no such facility.
Promises — at least in javascript which is the language observed here — are used to return asynchronous results and be able to combine them in various manners (chain, multiplex, select), but not to cancel them.
The purpose of promises in javascript is very much to improve upon callbacks-based systems in order to make them easier to understand and reason about, and more composable.
https://gist.github.com/darius/6326461
In Firefox Nightly it's usually slower than both of the OP's examples, while in Chrome it's substantially faster than both. So I'm curious how when.js goes faster on Firefox while supporting more features, and whether it could be tuned to catch up with my naive code on Chrome.
core.async looks interesting though; I assume I can't just consume it as a library and instead must rewrite my whole application in (clojurescript? you literally never mention in this post what language that is)? Too bad, but I can see how for this type of thing you need compiler support.
For clarity, core.async is a library that can be used in both Clojure and Clojurescript. There are internal implementation and performance differences, but the behavior of the library is the same for both.
Best writeup about that I've seen so far is here: http://lukevanderhart.com/2011/09/30/using-javascript-and-cl... (in the exporting section)
I'm not sure if there are any implications I'm not aware of that would make it less useful to use core.async from js land.
$ node --trace_inlining go_when.js
Did not inline fulfilled called from coerce (target contains unsupported syntax [early]).
Did not inline NearFulfilledProxy called from fulfilled (target requires context change).
Did not inline near called from fulfilled (target contains unsupported syntax [early]).
Did not inline Promise called from near (target requires context change).
Did not inline enqueue called from scheduleConsumers (target requires context change).
Did not inline b called from enqueue (target not inlineable).
Did not inline fn called from NearFulfilledProxy.when (target requires context change).
Also some functions are constantly being recompiled and try catch is corrupting (making them not optimizable under current v8) some large functions instead of being isolated.I grant that go/core.async-style channels are a superset of promise/future kind of functionality, but to me they are definitely part of the same family. And you can definitely implement promises in terms of them.
The big win isn't that you have the ability to await and fulfill messages, but rather that you have first class entities that can be used to send and receive discrete and coordinated messages.
What's better than simply having these communication channels (think event emitters) is having the ability through clear syntax to synchronize not only the receipt of these messages, but also to coordinate message sending through the same uniform style.
To me, this seems just about as related to promises as promises are related to callbacks, in that they are all patterns for asynchronous programming.
To drive the point home: "I grant that promises are a superset of callbacks kind of functionality, but to me they are definitely part of the same family. And you can definitely implement promises in terms of them."
Promises and channels are synchronization primitives. Callbacks are not.
Bold (and interesting) claims. I await the blog post substantiating these claims.
[1] https://github.com/mozilla/mozilla-central/blob/master/toolk...
[2] https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_...
[3] http://dxr.mozilla.org/mozilla-central/search?tree=mozilla-c...
Comparing the execution time of a hundred thousand (sync or not) callbacks vs. a hundred thousand (sync or not) promises is, in many cases, not a good or deciding factor for what tool you should pick from the toolbox. I would assert that in realistic scenarios, it really doesn't matter.
Next.