I'm looking for evidence of my observation that mo...
# thinking-together
e
I'm looking for evidence of my observation that most async functions (at least in JavaScript/TypeScript) are called with a direct await. I think most developers find the synchronous semantics much easier to understand and therefore make the code better maintainable. Only in a few specific places I see
.then(...)
or
Promise.all(...)
being used consistently without a final await. Do you know of any research backing this observation? If so, please leave a message (synchronous or asynchronous 😉) pointing me in the right direction. Thx!
t
but thats still useful? The motivation was to avoid having to write a series of steps in ever deepening nested
.then
blocks for relatively trivial yet serially dependent tasks like
fetch(...)
a.k.a. callback hell So even though only one promise is open is open at a time and there is 0 concurrency going on, a
then
makes this trivial and common task messy by introducing a callback and therefore nesting. Article 11 years ago before we had await. https://stackoverflow.com/questions/25098066/what-is-callback-hell-and-how-and-why-does-rx-solve-it You phrasing makes me thing you think developer who are not fanning out async with Promise.all() are doing it wrong. And if there task has real potential concurrency you would kinda be right, but because Javascript is single threaded you can still have the situation await leads to flatter code and there is no potential fanout to put a Promise.all So "callback hell" in the context of "promises" is the research that led to await. If you want direct evidence people use await next to an async function you could probably pick an open source project and measure it, I also believe it is probably very common.
а
personally I think async/await is a bloat, because multi-threading should defined by structure that "context changes between those block will never happen, therefore they do not depend on each other and should work in parallel"
t
its really not though, its still cooperative multitasking. Async is a signal that JS execution engine is free to do other work while something else in the background happens. Even in a straight serial block of asyncs, those asyncs are allowing stuff completely elsewhere to make progress by yielding execution. Because JS is single threaded and someone needs to do that otherwise the main thread is blocked. I love the JS execution model, having worked in other languages where execution can be interrupted right in the middle of a function execution, JS makes multitasking very easy to reason about, you function always executes fully.
а
I agree await solves the cooperative yield problem in JS specifically. My concern is that it conflates two things: the dependency structure (what must happen before what) and the scheduling mechanism (when the thread yields). In a system with structural concurrency, you'd express the dependencies explicitly — graph, not chain — and the host decides how to schedule. Await makes the chain look flat but the graph is still implicit and scattered across suspension points.
t
Yeah I am also a fan of that too, observable runtime is such an engine and it ends up removing the outer awaits. https://observablehq.com/@mbostock/introduction#cell-152
❤️ 1
e
My question was an observation which might have been biased by my preference for Smalltalk style 'concurrency', but certainly not a 'they are doing it wrong' statement. Smalltalk Processes (which are very light weight) run and can be pre-empted (not necessarily on a function boundary as JS, but not half way during a slot assignment). If you want to do things in parallel, you create another Process (again, which is cheap). This leads to code that (in my opinion) is easy to read and understand. Everything is synchronous. The concurrent parts are made explicit by adding Processes. And if needed you can wait on another Process to finish. In Smalltalk you'll have to add locks and guards if two Processes will update the same Object. Other approach. I like this style of programming, but can probably be a pita if you do a lot of concurrent stuff (not doing any heavy concurrent stuff, so I don't know). I'm developing a new language with many similarities to the Smalltalk model. I think the ease of understanding synchronous code ways in heavy. I was looking for some proof to back this up. If such proof ain't there, it will at best be hearsay and/or feeling. Is okay either way. If there is some proof/evidence I'd like to add it to my design decisions document. I'm not going to do my own statistical analysis on GH. Changes are it will be a lot of work and it is not worth it. (I'll make the decision based on feeling, just as easy 😉).
а
I was saying it's wrong not because it fails to describe an action, but rather because "logically" you have multiple ways to make independent execution from each other and it's wrong because it's redundant and you can't change it via substitution (changing how threads created and how do they run the code). I mean sure it's great to have a distinction between concurrency and multi-threading, but if your description of a program makes this distinction at the foundation that you can't override, now you introduce non-composability inside a language and taking away the choice of decision from the user. Basically It doesn't matter if your threads are light or heavy, if you leave that up to the user and implementation, but I think this might go beyond your goal.
e
@Андрей Бурлаков just to be clear, my reference to 'they are doing it wrong' was for Tom who thought I was judging developers, which I was not. I've never really paid too much attention to the deeper concepts of JS async stuff (except for sometimes having to cope with it in my Smalltalk where I bridge to JS). The point you make seems fair, but I'm not sure if I qualify to say so since I'm not very knowledgeable on the topic.
👍 1
❤️ 1
t
> Only in a few specific places I see
.then(...)
ok, well
.then
is virtually considered legacy in JS at this point. Its only really used in a synchronous function to throw off a side-effect that is unrelated to the return. That means you can avoid marking the function as async. async/await has replaced most uses of the Promise objects. Promise.all is still needed and used, because thats where you can get parallelism and therefore speedups. I am not sure they actually merged it but the main linter for JS has at least agreed stylistically
await
is preferred to
then
(https://github.com/eslint/eslint/issues/9649)
d
Can confirm this is also the case in Rust async, at least in the macro syntax sense. You have mostly procedural code and key pinch points will do
select!
(
Promise.race
) or
join!
(
Promise.all
). Usually, those are points of cross task/thread communication and scheduling or timeouts, with a lot of code falling into an event loop style as such. So roughly (bear with me, I don't know PL nomenclature), async must appear like "bloat" that shows up everywhere only to be exercised at these pinch points. At least on the macro syntax level. But! As Tom points out, cooperative multitasking using futures/async-await/delimited continuations (all in service of having stackless coroutines) is such a win that a lot of trains have tacked on that car. For the rust case, if you're familiar with the lang, check out this discussion https://without.boats/blog/let-futures-be-futures/ Note that rust is not JS, it's not forced into a single threaded runtime and yet mountains were moved to implement this feature. And think about cooperative multitasking/concurrency conceptually. The CPU gives us one temporal line to follow. One way or another, you'll have to pay the toll to hack around this weather it's stackfull/stackless coroutines or preemptive green threads. The syntax being the major "cost" in the stackless case.
🤔 2
🍰 2
t
that futures-be-futures was a great article.
d
The whole blog is a must if you write Rust. Explains and contextualizes some of its weird bits in very clear terms.