Let me show a quick comparison of JavaScript's Promises, and Python's futures, where I can point out the main usecases and reveal the
reasons behind decisions.
I will use the following dummy example to demonstrate the use of async functions:
async function concatNamesById(id1, id2) {
return (await getNameById(id1)) + ', ' + (await getNameById(id2));
}
Asynchronous JavaScript
Back in the day, before the concept of Promises emerged, people wrote their code using callbacks. There are still various conventions about
which argument should be the callback, how errors should be handled, etc... At the end, our function looks something like this:
// using callbacks
function concatNamesById(id1, id2, callback) {
getNameById(id1, function(err, name1) {
if (err) {
callback(err);
} else {
getNameById(id2, function(err, name2) {
if (err) {
callback(err);
} else {
callback(null, name1 + ', ' + name2);
}
});
}
});
}
This does the same as the example, and yes, I used 4 indentation spaces intentionally to magnify the problem with the so called callback hell or
pyramid of doom. People using JavaScript were writing code like this for many years!
Then Kris Kowal came with his flaming Q library and saved the disappointed JavaScript community by introducing the concept of Promises.
The name is intentionally not "future" or "task". The main goal of the Promise concept is to get rid of the pyramid. To achieve this promises
have a then
method which not only allows you to subscribe to the event that is fired when the promised value is obtained, but will also
return another promise, allowing chaining. That is what make Promises and futures a different concept. Promises are a bit more.
// using chained promises
function concatNamesById(id1, id2) {
var name1;
return getNameById(id1).then(function(temp) {
name1 = temp;
return getNameById(id2); // Here we return a promise from 'then'
}) // this then returns a new promise, resolving to 'getNameById(id2)', allows chaining
.then(function(name2) {
return name1 + ', ' + name2; // Here we return an immediate value from then
}); // the final then also returns a promise, which is ultimately returned
}
See? It is essential to unwrap promises returned from then
callbacks to build a clean, transparent chain. (I myself wrote this kind of asynchronouos
code for over a year.)
However, things get complicated when you need some control flow like conditional branching or loops.
When the first compilers/transpilers (like 6to5) for ES6 appeared, people slowly started to use generators. ES6 generators are two directional, meaning
the generator not only produces values, but can receive supplied values on each iteration. This allowed us to write the following code:
// using generators and promises
const concatNamesById = Q.async(function*(id1, id2) {
return (yield getNameById(id1)) + ', ' + (yield getNameById(id2));
});
Still using promises, Q.async
makes an async function from a generator. There is no black magic there, this wrapper function is implemented using
nothing but promise.then
(more or less). We are nearly there.
Today, since the ES7 specification for async-await is pretty mature, anyone can compile asynchronous ES7 code to ES5 using BabelJS.
// using real async-await
async function concatNamesById(id1, id2) {
return (await getNameById(id1)) + ', ' + (await getNameById(id2));
}
Returning promise from an async function
So this works:
async foo() {
return /* await */ sleep('bar', 1000);
// No await is needed!
}
and so does this:
async foo() {
return await await await 'bar';
// You can write await pretty much everywhere you want!
}
This kind of weak/dynamic/duck typing fits well in JavaScript's view of the world.
You are right that you can return a promise from an async function without await, and it gets unwinded.
It is not really a decision, but is a direct consequence of how promise.then
works, as it unwinds the
promises to make chaining comfortable. Still, I think it is a good practice to write await before every
async call to make it clear you are aware of the call being asynchronous. We do have multiple bugs
every day because of missing await keywords, as they will not cause instantaneous errors, just a bunch
of random tasks running in parallel. I love debugging them. Seriously.
Asynchronous Python
Let's see what Python people did before async-await coroutines were introduced in python:
def concatNamesById(id1, id2):
return getNameById(id1) + ', ' + getNameById(id2);
Wait what? Where are the futures? Where is the callback pyramid? The point is that Python people
do not have any of the problems JavaScript people had. They just use blocking calls.
The big difference between JavaScript and Python
So why didn't JavaScript people use blocking calls? Because they couldn't! Well, they wanted to. Believe me.
Before they introduced WebWorkers all JavaScript code ran on the gui thread, and any blocking call caused
the ui to freeze! That is undesireable, so people writing the specifications did everything to prevent such things.
As of today, the only ways to block the UI thread in a browser I am aware of:
- Using XMLHttpRequest with the deprecated
async = false
option
- Using a spin-wait
- (Or do actual heavy computation)
Currently you cannot implement spin-locks and similar things in JavaScript, there is just no way.
(Until browser vendors start to implement things like Shared Array Buffers, which I am afraid of will bring
a special kind of hell upon us as soon as enthusiast amateurs will start to use them)
On the other hand there is nothing wrong with blocking calls in Python as usually there is no such thing as a
'gui thread'. If you still need some parallelism, you can start a new thread, and work on that. This is useful when
you want to run multiple SOAP requests at once, but not as useful when you want to utilize the computational power
of all the cpu cores in your laptop, as the Global Interpreter Lock will prevent you to do so. (This was
worked around by the multiprocessing module, but that's another story)
So, why do Python people need coroutines? The main answer is that reactive programming got really popular nowadays.
There are of course other aspects, like not wanting to start a new thread for every restful query you make, (some
Python libraries are known to leak thread ids until they eventually crash) or just wanting to get rid of all the
unnecessary multithreading primitives like mutexes and semaphores. (I mean those primitives can be omitted if your
code can be rewritten to coroutines. Sure they are needed when you do real multithreading.) And that is why futures
were developed.
Python's futures do not allow chaining in any form. They are not intended to be used that way. Remember, JavaScript's
promises were to change the pyramid scheme to a nice chain scheme, so unwinding was necessary. But automatic
unwinding needs specific code to be written, and needs the future resolution to make distinction between supplied
values by their types or properties. That is, it would be more complex (=== harder to debug), and would be a
little step towards weak typing, which is against the main principles of python. Python's futures are lightweight,
clean and easy to understand. They just don't need automatic unwinding.