Async will continue to look awkward until we drop explicit "await" on function calls. Awaiting should be the default. If I call "async foo()" then I get back a promise I can explicitly await, but there should be no semantic difference between calling a synchronous function or asynchronous without modifiers.
Not only it makes it more consistent, but you can implement running a synchronous function into a separate thread just by doing "async foo()". Can't get easier.
Lua already has the right idea here (check coroutines), and why it's so useful in game scripts.
All functions are async, calling functions does an implicit await, and the go keyword let's you spawn a task without await. (At least that's what semantically happens) This way you can write seemingly imperative blocking code, and have it work fully asynchronously.
It's great, freeing, and avoids the function coloring problem. It's really the feature I miss most when writing other languages.
Though I do understand why performance oriented languages like Rust might want to approach it differently.
I don't, however, see a reason for non-performance oriented ones to use function coloring (unless they're doing full semicolon customization, like Haskell with its monad stacks).
Performance-oriented languages tend to have a strong type system which lets them know well ahead of time what function is being called, so I see no reason why what you describe can't be done there without performance overhead.
There is a presumption of some runtime behaviors in the Go language spec that makes me wonder here … details can be lifted into the type system but if we’re trying to eliminate overhead, can we do this without revisiting a lot of the issues we were trying to solve with interesting concurrency primitives?
I keep hearing this term "requires a runtime" when async execution is thrown around. And on first glance it makes sense. On deeper look, technically all languages have SOME runtime behavior. And I don't know why we're so objected to it.
Presumably it can get weird when you call into a library and just blocks you. Thus informing us of the need of a common event loop. OS level. Which GUIs had from the get go, but apparently it's not just about GUI.
Like, I see ‘common event loop’ here and don’t OSs (and hardware) past a certain size generally provide interrupt handling, threading, memory management and so forth? These are all highly general mechanisms that have evolved to meet concurrent execution models… because of the generality, a lot of relevant things remain at a lower level of abstraction than languages often expose.
What I wonder about is what happens when we lift runtime policy decisions into the type system, in a way that allows something more performant - I think I have a sense of what this pans out like and I just am skeptical than for every case (for many, many cases, it seems like a great idea!) that this approach leads to the best implementation. It reframes a lot of the inherent difficulties, I’m not sure it helps abstract over them.
Basically we need hardware that works a lot more closely with the OS, and an OS that works a lot more closely with modern language semantics. The problem with the general abstractions you call out (correctly) as inefficient, is that they're stuck in the 80s, and they gotta work with code that looks pretty much like compiled C binaries from the 80s.
Green threads at the language level and so on is a stop-gap in the long-term. Everything has to align together, and then native thread switching will be just as (or almost as) light as green threads.
To be a tad more specific, basically we need hardware+OS to provide us with a model that's pretty close to what the Erlang VM emulates.
What you are describing is really “async-never.” And in fact, the programming model that async/await gives you is in fact, blocking. To the programmer, awaiting is no different from blocking a function.
In practice though, async/await is a specific way to implement a blocking programming model with non-blocking internals. It basically breaks functions down into state machines so they can be executed incrementally. The other way is basically lightweight threads with semi-cooperative scheduling, like Go does, and what you could do in Lua (though Lua cothreads are technically quite different and their own bag of worms.)
This model works nicely in Go, whose lightweight threads pay the cost of C interop in some nanoseconds of overhead, and whose compiler adds yields in key points to aid platforms that can’t (yet) support some form of usermode preemption.
However, in JS, we already have the event loop. Control always returns back to the event loop. This has worked this way for decades in most places where JS is used, so it’s unlikely to change soon. Because of this, functions in JS can’t block. async/await is ideal here because the decomposed functions can return to the event loop when they await, for a callback to trigger the next fragment.
And in Rust, the language has use cases and environments that it runs in where the runtime costs and requirements of a lightweight thread scheduler are not acceptable. So yet again, async/await is ideal, because it allows for a blocking programming model that doesn’t compromise on the other harder requirements.
So then, should all functions just be “async”, and all function calls be “awaited?” I think this will prove complex and still undesirable. For one thing, async is hard in statically compiled languages. Your state will generally be the sum of the state of functions you await, which proves very complex for say, recursive async functions. Even when not, async functions require some kind of scheduler that manages all of the running routines. In a low level language, you may not want to impose an async routine scheduler or even async/await in the first place onto applications.
It’s unfortunate, but I think we’re finding at least local maxima to the solutions to async programming, and running into limits that appear harder to conquer. A blocking programming model is convenient, but if you want a blocking programming model with non-blocking behavior, it can’t come entirely “free.”
> What you are describing is really “async-never.” And in fact, the programming model that async/await gives you is in fact, blocking. To the programmer, awaiting is no different from blocking a function.
Not exactly, I rather proposed async calls are explicit, instead of blocking calls being explicit.
Most people have no problem with the APPEARANCE of blocking. They mind if it's actually blocking under the hood. It's normal that when you issue some HTTP request, for ex. you can't proceed processing the response until you get it. One way or another "you await" that moment.
Regarding whether it's free or not, I still don't quite understand the problem TBH. An event loop isn't a hard addition to any language.
The only thing I wonder about is stack fragmentation.
I would also like awaiting to be the default, as I've been bitten more than a few times by forgetting to await.
It's tricky though -- if I assign the result of a function to a variable, should I immediately await? What if I'm creating a temporary to pass into Promise.all or Promise.race? Perhaps not-awaiting needs to be explicit? I'm not sure.
Async/await is awkward (or has been in languages I've used it in), but I don't think it's because of the await.
It's because await foo(); (or however you write it) doesn't tend to act the same as a synchronous function, even though that's what you're asking for. Because you called an async function, even though you immediately await the results, now your function is async, and it cascades up the chain, but often you need to provide a non-async interface, so then you're really stuck.
Javascript has an inflexible execution model, so you kind of need to take what you can get, but I prefer an explicit threadish thing plus async messaging. In your thread, you can (async) message another thread (or spawn a thread with an initial message) and wait for a response when you're ready to block, which could be immediately... or just send the message and loop on incoming messages to react appropriately if you get a reply.
> It's because await foo(); (or however you write it) doesn't tend to act the same as a synchronous function, even though that's what you're asking for. Because you called an async function, even though you immediately await the results, now your function is async, and it cascades up the chain, but often you need to provide a non-async interface, so then you're really stuck.
As far as I'm aware of, this is pretty standard for language-level concurrency sugar. It's syntactically blocking in that it allows you to write code without callbacks/message handlers, but it's infectious up the call stack.
I’d like to see a way to opt in to this behavior, for example with a “use await” within a function or module, similar to “use strict” but without making it a global setting.
Not necessarily; you can remove the cumbersome async/await syntax while still tracking concurrency/mutation/control effects with an effect system, which might come with effect inference.
Also, even without effect inference, implicit effects don't have to be wrong: Haskell has them (unsafePerformIO) as do other MLs, but they aren't used in normal programming and that's largely turned out fine.
Not only it makes it more consistent, but you can implement running a synchronous function into a separate thread just by doing "async foo()". Can't get easier.
Lua already has the right idea here (check coroutines), and why it's so useful in game scripts.