Better JavaScript Promises
frederic.vanderessen.com
frederic.vanderessen.com
In particular, the following code snipped, copied from the blog post, goes against everything we've been taught by modern programming languages:
var a = future;
var b = a;
console.log(a,b); // will print 'hello' 'world'
b = 'world'; // this does not resolves 'a'
a = 'hello';
It breaks the concept of variables being references to objects. This is similar to Matlab's treatment of matrices, that none of it's successor languages (Python/NumPy, Julia) emulates.Furthermore, it's confusing in the way that `a` could mean both (1) get value referenced by variable `a`, and (2) wait until value referenced by `a` is available. I believe that a much better proposal would be to treat futures as a separate data type (`a = future()`), and implement getting and fulfilling the future as method calls (`a.set(1)` and `a.wait()`).
More importantly, even if all the code is written as nicely as the example above you still cannot reason about this like you could about every other goddamn language out there. You can't pick arbitrary lines of code from the listing and execute them in that order, and if you try to figure out how it works (even if it's written without deadlocks) you will break your brain.
Instead, let's something like $q [1], or even just the smaller/simpler AngularJS implementation of it [2]. While requiring callbacks, it's still very workable and does simplify things. Adding chained callbacks is very nice:
var resp = $.get('/my/url');
showLoadingScreen();
resp.then(function(data) {
return JSON.parse(data);
}
resp.then(function(data) {
alert('data.username = ' + data.username);
}
resp.catch(function(err) {
alert('Error: ' + err);
})
resp.finally(function() {
hideLoadingScreen();
})
[1] https://github.com/kriskowal/q/wiki/API-Reference[1] Note, however, that it would turn JS's threading model completely on its head. Right now, the philosophy of JS is "single-process" threading model, with WebWorkers supporting a "multi-processing" concurrency model by isolating different threads of control. Allowing futures without callbacks would bring JS closer to Golang's concurrency model.
First, depsite the ugliness that asynchronous code entails, it does have one very important feature: it never blocks, which makes reasoning about it much easier. This proposal would make it possible for Javascript code to block, which enables a whole class of bugs that were impossible before.
var a = future;
var b = future;
setTimeout(function() {
a = b+1;
},0);
setTimeout(function() {
b = a+1;
},0);
console.log(a, b);
Hello deadlocks!Second, futures aren't first-class values. The proposal specifies that assigning a future to a variable doesn't create a second reference to the future, it creates a child future that would be resolve by the resolution of the parent future.
Thus, a future must be resolved in the scope in which it's created, or the resolution won't be propagated to all its children. With promises, you dont' have to worry about the side effects of assignment or function calls. Take this common pattern for wrapping an asynchronous call with a promise:
function promiseMeSomething() {
var d = defer();
doSomethingAsyncronous(function(err, value) {
d.resolve(value);
});
return d.promise;
}
We could do the same with futures: function futureMeSomething() {
var f = future;
doSomethingAsyncronous(function(err, value) {
f = value;
});
return f;
}
But that callback function is repetitive, so we'll refactor: function makeCallback(d) {
return function(err, value) {
d.resolve(value);
}
}
function promiseMeSomething() {
var d = defer();
doSomethingAsyncronous(makeCallback(d));
return d.promise;
}
But consider the same refactoring with futures: function makeCallback(g) {
return function(err, value) {
g = value;
}
}
function futureMeSomething() {
var f = future;
doSomethingAsyncronous(makeCallback(f));
return f;
}
Whoops. This won't work, because g is a child future of
f and resolving it won't resolve f.Asynchronous effects need to remain explicit.
Consider a simple example:
function binary(stack,combine) {
var a = stack.pop();
var b = stack.pop();
stack.push(combine(a,b));
}
In today's JavaScript, this code executes as an atomic sequence. It turns [..,a,b] into [..,combine(a,b)] and there is no way for another piece of code to step on this function's toes and break this invariant.What if "combine()" suddenly became asynchronous ? Either because its operation itself is asynchronous, or the evaluation of "a" or "b" involved an asynchronous operation. This would cause the call to "binary()" to pause in the middle of the function, which would give time to another part of your code to call "binary()" in turn. And now [a,b,c,d] has turned into [a + b, c + d] instead of [a, b + c + d], and you'll have a jolly good old time debugging tht.
Make asynchronous context switches implicit and you start needing locking primitives. Everywhere, including third party libraries that "used to work". Because every single line of code(except plain assignment) could be a ticking async-bomb that will pause the current function and let other functions wreak havoc on your invariants.
Scary.
Callbacks are ugly, and promises are hardly better, but asynchronous operations NEED to stay explicit. And explicit asynchronous operations are contagious. That's just what they are.
Just make the syntax less right-pyramidal: C#'s async/await syntax is a good candidate, and 'await' is a good way to mark a possible context switch.
http://wiki.ecmascript.org/doku.php?id=strawman:async_functi...
I feel that the author intuited that something like this would be nice, but wasn't aware of async/await and so proposed something that only kind of vaguely points in the right direction. I don't think he's wrong that something like this would be nice, but async/await is a much more robust way of dealing with this!
Check out co (https://github.com/visionmedia/co/blob/master/index.js). For all its features it's tiny. Increased payload and incompatibility is a blanket statement; that could apply to any library or framework.
> And I am also quite disappointed that the solution to one of JavaScripts biggest flaws requires hacking a language feature that was designed for an entirely different use-case, which will surely bring lots of confusion.
There should be a really high bar for adding keywords to a language. If something can be solved with features we already have, we needn't add another. ES6 generators solves the callback problem for now. There are of course other (perhaps more flexible) solutions, like Scala's futures and for-comprehensions [1], or F# computation expressions [2]. But generators are simpler and they work well enough.
[1] http://docs.scala-lang.org/overviews/core/futures.html [2] http://fsharpforfunandprofit.com/series/computation-expressi...
I'm not at all sure the method described in the linked article would work as well as said article makes it sound, but I'm definitely in favor of seeing people much smarter than I am argue this one to a finish and then implement the last proposal standing.
var a = future;
console.log(a); // will print 'hello'
a = 'Hello'
a += ' World!'
console.log(a); // will print 'Hello World!'
Deferreds/promises are good exactly because they explicitly state what they are through their type, and they behave the "same" whether they are resolved or not.This proposal breaks the notion of what a variable is, and it behaves very differently whether the future has been replaced by a value or not. This will become confusing very fast and a great source of race conditions.
var a = future; setTimeout(function(){ a = 'Hello World'; },1000); console.log(a);
If evaluating 'future' returns control to the scheduler, the setTimeout never gets called.
On a higher level, these "flow control" libraries do not, despite their name, control the flow of logic. The underlying asynchrony remains exactly the same. They just make it easier to avoid an indentation avalanche, or callback spaghetti. If and only if spaghetti makes it difficult for you to reason about code will these "flow controls" make it easier to reason about asynchronous Javascript.
- passing it as an argument to an operator or built-in function
- accessing one of its its members
- calling it as a function
You can think of these deferred values as being a getter and setter pair, sharing a reference:
var future = function() {
var x;
return {'getter': function () {
while (typeof(x) === 'undefined') wait;
return x;
},
'setter': function(val) {
x = val;
}};
};
In the article, future is a keyword rather than a function, and the getter and setter are called implicitly. The idea is that we can pass around references (the {'getter', 'setter'} pair) however we like without triggering any action; however, once the getter is called it will enter a loop, polling and 'waiting' (switching execution back to the scheduler) as long as the setter's not been called.The radical part here is the 'wait', which I've made up to illustrate the point, and is the crux of the whole thing. It's basically a cooperative multithreading mechanism. This would let an asynchronous API be turned into a synchronous one, since we just need to wait for the result with a polling loop like my example above.
Whether that's a good idea or not is debatable. Personally I think it's a bad idea; shared mutable state is the cause of all concurrency problems (race conditions, locks, deadlock, livelock, etc.) so it absolutely should not be default or implicit. The idea that 'concurrent' == 'multithreaded' needs to die; multithreading is a dangerous anti-feature which uses a little syntax to hide a whole new language semantics, breaking all intuitions about our code and throwing safety properties out of the window.
Thankfully Javascript hasn't fallen into this trap yet; eg. WebWorkers uses message-passing instead.
>Unfortunately, using yield based solution will require third party libraries, which means increased payload and incompatibilities among frameworks.
Do you mean that translating to yield requires wrapping things that aren't expecting it? That's true of moving to any non-callback solution. Or do you mean that you want to see the native Node APIs implemented with generators rather than callbacks? I hope the solution to that will simply be patience.
I have had good luck using javascript promise objects to clean up complicated async calls.
for example, making multiple calls and waiting for all responses looks something like this:
promise = YourFavoriteAsyncLib.all([new Promise('/restaurants'), new Promise('/shops')]) promise.then(function(res) { // res[0] === restaurants // res[1] === shops });
...or we can wait for one to finish and then make a couple more calls...
promise = new Promise('/restaurant/1');
promise.then(function(r) { return YourFavoriteLibAgain.all([ new Promise('/restaurant/' + r.id + '/patrons'), new Promise('/restaurant/' + r.id + '/menu') ]); }) .then(function(res) { // do more fun things synchronously });
The details change, and some libraries act a little differently, but once you break they async calls into steps it becomes easy to read.
I think the point of all of these responses is that async cannot look like regular statements because they do not act like regular statements.
Personally, what I'd really like to see is an M:1 thread model, i.e. a model where you program as if you have M threads of control, but only one of them is actually executing at any one time. Any calls which are currently async would behave as if blocking and would yield control to any other runnable "thread". So you would have concurrency but not parallelism -- which is effectively what you have to reason about anyway when you have async.
It may very well just be me, but I find that reasoning about normal "blocking" flow control is vastly easier in general than reasoning about async flow control even if it involves multiple threads. It gets even easier if you limit all inter-thread communication to some form of queueing/message passing.
EDIT: Reading it back this looks overly convoluted, basically what I'd like in JS is "user-space threads".
https://github.com/clojure/core.async/blob/master/examples/w...
Promise.all([async1, async2, async3]).then(function(results) {
console.log('async1:', results[0]);
console.log('async2:', results[1]);
console.log('async3:', results[2]);
});
or: Promise.all([async1, async2, async3]).spread(function(res1, res2, res3) {
console.log('async1:', res1);
console.log('async2:', res2);
console.log('async3:', res3);
});
With the co library (https://github.com/visionmedia/co), it'd look like this: co(function*() {
var results = yield [async1, async2, async3];
console.log('async1:', results[0]);
console.log('async2:', results[1]);
console.log('async3:', results[2]);
})();
Until ES6 is available everywhere, the existing promises libraries are so mature and easy-to-use that I don't really see a problem with them. Bluebird will even promisify an entire module for you with `promisifyAll`, so mixing it with callback-based modules is typically trivial.I think that even though it relies on the use of third-party libraries like co, the generators solution is much easier to reason about than the proposed solution here.
I can't find the source, unfortunately, but I remember reading somewhere that generators were actually designed with this use case in mind, so the claim that using them for async "requires hacking a language feature that was designed for an entirely different use-case" is false, if I'm remembering correctly.
For example, Mongoose takes all of methods on the prototype during object creation and replaces them with ones that run middleware. This means that if you try to use one of the promisified methods, none of the middleware gets called.
[1] https://github.com/petkaantonov/bluebird/commit/70f90c33dd94...
So here's the (shorter) list of issues, which I'll support later on with code samples:
1. The new behavior is invisible to most readings of the source code. It's just magic, and unless you've obsessively tracked the magic as it flows, you never know where it might be.
2. The magic can strike in any place, at any time, and can throw an exception when it happens. Even `a=b+c` is no longer safe.
3. Even when it doesn't throw an exception, the magic can cause anywhere/anytime pauses and delays. Good luck with that smooth animation function.
4. The assignment semantics mean that even the equals sign is not safe, and can fuck up my code.
5. Infinite loops?
6. Indefinite resolution?
______________
#1:
Should be self-evident. There's no way to know, reading code, whether a variable is a future, unless you backtrack it alllll the way. There are no annotations or markings as it flows.
That would be par-for-the-course in JS and fine except that we're not just talking about another scalar type, this is a whole new beast.
#2:
future function getCount(){
raise "OH GOD THE NETWORK IS DOWN";
}
var b = 7;
var c = getCount();
var a = b + c; // How was I supposed to expect an exception here!?
#3: var congratulationAnimation = function(msg){
// Spend 1 second fading in different UI elements
var escaped = escapeForHtml(msg);
// Spend 1 second fading in message
// Spend 1 second fading in remainder of UI elements
};
var msg = getMessage(); // Secretly a future from somewhere
congratulationAnimation(msg);
// WTF, why is my animation pausing halfway!?!?
#4: var msg = future;
alert(msg);
// [...elsewhere...]
// Store heroic status update
msg = "Today I beat up a baby";
msg += " godzilla which was terrorizing a local school";
// OH GOD THEY THINK I'M A VILLAIN NOW
#5: // I have no clue what this'll do.
var a = future;
a = 1 + a;
#6: // What does this do, break console output forever?
var f = function(){
var a = future;
console.log(a);
}
f();And I know this seems petty, and for that I apologize, but... "excape"?
A few years ago it's what everyone liked about node ...
You would write this:
var! x = expression;
statement;
statement;
And JavaScript would parse it as: return expression.then(function(x) {
statement;
statement;
});
This is obviously shamelessly pilfered from monadic syntax 'let!' in F#, and my own 'let!' extension in OCaml.Improve them slightly add error handling like maybe monad. Yield has done wonders for the delegate allergic people in C# for ages.
We probably should abstain for a while to change the JS language - it is quite messy as it is. Undescore is awesome example how you can extend the language with small amounts of code.
If I was writing localStorage I would use setTimeout or something and make it async from the beginning. You should always assume async when any type of I/O is happening.
The point of Node is that it is asynchronous.