Hacker News new | ask | show | jobs
by bluepnume 859 days ago
This might be a silly question, but what would be the downside of a single threaded language similar to javascript, but where every function is just 'async capable' and can always be non-blocking, with no special syntax like async/await?

So every time you call foo(); you anticipate that it might resolve immediately, or it might take some time, but it won't block any other function (unless it actually does some cpu bound operation)

And any time foo(); does something asynchronous, all of its callers, and its callers callers, become implicitly asynchronous too.

Of course you would have to have some primitive for when you actually want to do several things concurrently within the scope of a single function rather than blocking that function, but that doesn't sound too bad and in JavaScript you effectively need to use Promise.all most of the time anyway.

I'm sure there's some major downside I'm missing -- but what is it?

5 comments

You can do that, Java did recently. You need full control of all the blocking primitives so they can yield the thread.

Where this breaks is with FFI: if you cannot intercept blocking calls in foreign functions, you block useful threads or even deadlock. This is what the quote in the article is about:

> The cost for the native compatibility with the C/system runtime is the “function coloring” problem.

This is what I describe as "stackful coroutines" in the final section of the post.
Is stackfulness required for what the grandparent describes? It seems possible to do this stacklessly: all functions implicitly compile down to a state machine, but futures are never visible.
You need stackful coroutines or continuations (which become really just a kind of growable stack) if you want recursive functions. This is a limitation of Rust’s design.
I just want a language where await is the default behavior and I have to specify when I want to capture a future as a value and do something with it later. Most of the time I want an emulation of a synchronous thread of execution even if it's actually async. It's crazy to have to constantly specify the common case and to have the possibility of surprising behavior at best if I accidentally forget to await something somewhere.
"T" and "impl Future<Output = T>" are very different types so unless you're doing very silly things I don't think you can be "surprised" in Rust. The fact that await points are explicitly marked and visible in the code is a feature, not a bug. It's similar to the case for "?" vs. hidden exceptions.
Generally speaking async code in a language that isn't ground up designed around that is far slower, because the language ends up using some relatively expensive mechanism like mutexs. In most languages making 90% of your code 90% slower but "everything async" isn't really a worthwhile general trade off.

Even is your language run time is fully async, it's really hard to do anything useful without touching C/C++/the kernel/system libraries, and as soon as you do that all those guarantees go out the window, because you cannot rewrite the world in $newlang.

How could the language know when an operation is going to be cpu bound?
... try asking ChatGPT? ;-)