4 ms·
> Language designers who studied the async/await experience in other ecosystems concluded that the costs of function coloring outweigh the benefits and chose di
by kibwen 6mo ago
> Language designers who studied the async/await experience in other ecosystems concluded that the costs of function coloring outweigh the benefits and chose different paths.
Not really. The author provides Go as evidence, but Go's CSP-based approach far predates the popularity of async/await. Meanwhile, Zig's approach still has function coloring, it's just that one color is "I/O function" and the other is "non-I/O function". And this isn't a problem! Function coloring is fine in many contexts, especially in languages that seek to give the user low-level control! I feel like I'm taking crazy pills every time people harp about function coloring as though it were something deplorable. It's just a bad way of talking about effect systems, which are extremely useful. And sure, if you want to have a high-level managed language like Go with an intrusive runtime, then you can build an abstraction that dynamically papers over the difference at some runtime cost (this is probably the uniformly correct choice for high-level languages, like dynamic or scripting languages (although it must be said that Go's approach to concurrency in general leaves much to be desired (I'm begging people to learn about structured concurrency))).
- tekacs 6mo agoI mean Java's Loom feels like the 'ultimate' example of the latter for the _ordinary_ programmer, in that it effectively leaves you just doing what looks like completely normal threads however you so please, and it all 'just works'.
- ysleepy 6mo agoJava has gone full circle. Java had green threads in 1997, removed them in 2000 and brought them back properly now as virtual threads. I'm kinda glad they've sat out the async mania, with virtual threads/goroutines, the async stuff just feels like lipstick on a pig. Debugging, stacktrackes etc. are just jumbled.
- gf000 6mo agoI don't think comparing 97's green threads to virtual threads ever made sense. Like their purpose/implementation everything is just so different, they don't share anything at all.
- delusional 6mo agoJava didn't really "sit it out". It launched CompletableFutures, CompletionStages, Sources and Sinks, arguably even streams. All of those are standard library forms of async programming. People tried to make it catch on, but the experience of using it, The runtime wrapping all your errors in completion exceptions, destroying your callstacks, just made it completely useless.
- jen20 6mo agoEvery Java codebase using something like Flux serves as a datapoint in favor of this argument - they're an abomination to read, reason about or (heaven help) debug.
- iknowstuff 6mo agoIn Rust debugging and stacktraces are perfectly fine because async/futures compile to a perfect state machine.
- comex 6mo agoThey are not perfectly fine. If a task panics then you will get the right stack trace, but there is no way to get a stack trace for a task that’s currently waiting. (At least not without intrusive hacks.)
- tcfhgj 6mo agoWould this be considered an intrusive hack? https://docs.rs/tokio/latest/%20tokio/runtime/struct.Handle.html#method.dump https://docs.rs/tokio/latest/%20tokio/runtime/struct.Handle....
- rwoerz 6mo agoThey stopped at the Promises level with CompletableFuture that lead to "colored frameworks" like WebMVC vs. WebFlux in Spring.
- Sankozi 6mo agoWho is they? Java has moved past those promise based API and avoided async/await mistake.
- sqquima 6mo agoI'm curious how escape analysis works with virtual threads. With the asynchronous model, an object local to a function will be migrated to the old generation heap while the external call gets executed. With virtual threads I imagine the object remains in the virtual thread "stack", therefore reducing pressure in garbage collection.
- Rapzid 6mo agoThe initial Loom didn't really provide the semantics and ergonomics of async/await which is why they immediately started working on structured concurrency. And for my money I prefer async/await to the structured concurrency stuff..
- YZF 6mo agoBoost.Asio (2005) is surely worth a mention. But the pattern predates this by decades. Green threads, what Goroutines are, comes from the 1990's.
- leoc 6mo agoWhat should people read to learn about structured concurrency?
- gf000 6mo agoPerhaps java's related JEPs could be a good starting point? https://openjdk.org/jeps/505 https://openjdk.org/jeps/505 There are also related discussions on other platforms that are worthy to read.
- kibwen 6mo agoI think the clearest sales pitch comes from this post from the author of Trio, which is an implementation of structured concurrency for Python: https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ https://vorpus.org/blog/notes-on-structured-concurrency-or-g... .
- leoc 6mo agoThank you. So it's more or less running (certain specified) function calls in parallel? Sounds nice, but what happens to upward funargs? I assume first-class continuations are right out ...
- jeremyjh 6mo agoCSP is a theory about synchronization and implies nothing about green threads or M:N scheduling. Go could have used OS threads and called it CSP. Certainly it’s true that Go invented neither, both Erlang and Haskell had truly parallel green threads without function coloring before Go or Node existed.
- simonask 6mo agoI agree with you, but the big difference between function arguments and effect systems is that the tools we have for composing functions with arguments are a lot simpler to deal with than the tools we have for composing effects. You could imagine a programming language that expressed “comptime” as a function argument of a type that is only constructible at compile-time. And one for runtime as well, and then functions that can do both can take the sum type “comptime | runtime”.
- scuff3d 6mo agoIn my experience people complain about it because they are coming from a blocking first mindset. They're trying to shoehorn async calls into an inherently synchronous structure. A while back I just started leaning in. I write a lot of Python at work, and anytime I have to use a library that's relies on asyncio, I just write the entire damn app as an asynchronous one. Makes function coloring a non-issue. If I'm in a situation where the two have to coexist, the async runtime gets its own thread and communication back and forth is handled at specific boundaries.
- otabdeveloper4 6mo ago> Makes function coloring a non-issue. Yes, having to rewrite literally all of your code because you need to use an async function somewhere is an issue. An even bigger issue is that now you have two (incompatible!) versions of literally every library dependency.
- scuff3d 6mo agoI'm usually writing applications, not libraries, so it's a non-issue for me. I was talking about when writing from scratch.
- coldtea 6mo ago>In my experience people complain about it because they are coming from a blocking first mindset. They're trying to shoehorn async calls into an inherently synchronous structure. There's no "inherently synchronous structure", at least not in Javascript. The nature is synchronous, asynchronous is an illusion built on top of it. Which is why you can easily block an "asynchronous" program: while (true) {} on any async function will do. JavaScript execution is synchronous on a single call stack. That's why they added Workers which is different to async. Rust's Tokio and co are also blocking. You need threads to get something that's not an inherently synchronous with merely a facade or cooperative asychronicity.
- scuff3d 6mo agoYou're blending concepts. All parallelism is asynchronous, but not all "asynchrony" is parallel. I have to use Python as as an example since I don't have much experience with JavaScript, but when you're using asyncio, that's single threaded non-blocking IO (asynchronous). Each use of "await" yields execution back to the event loop, and it can can schedule some other task to run. Tasks can run asynchronously, but no in parallel. When you use the multiprocess library you are actually creating new threads that run in parallel (I'm ignoring the threading library because it just muddies the water). That's also asynchronous execution. I don't know the semantics as well in JavaScript, but I'm sure the principle is the same. At certain points you can yield to the runtime and it will schedule other pending tasks to execute while the current one is pending. My point was that mistake I see people make (in Python) is they think of their program in blocking terms by default. So they get this frustrating coloring problem because they are trying to shoehorn in non-blocking calls. If instead you design the application from the start with asyncio in mind, it makes things much simpler.
- cma256 6mo agoThat is an unfair characterization of Zig. The OP correctly points out: > Function signatures don’t change based on how they’re scheduled, and async/await become library functions rather than language keywords. The functions have the same calling conventions regardless of IO implementation. Functions return data and not promises, callbacks, or futures. Dependency injection is not function coloring.
- dwattttt 6mo agoThese things _are_ function colouring, but they show function colouring isn't scary or hard. The original function colouring essay was much more about JavaScript's implementation than a general statement. If JavaScript had exposed a way for a synchronous function to call back into the runtime to wait for an async function to complete, it would still be just as coloured, but no one would be complaining about colour (deadlocks yes, but that's another kettle of fish).
- jwarden 6mo agoI think this is right. More specifically, the problem is JavaScript function colors mean Sync/Async, whereas Zig's mean Non-IO/IO. Using function colors for async is fundamentally unnecessary, whereas for I/O it is fundamentally necessary. You should be able to define a synchronous function that calls an asynchronous function. But it makes no sense to define a non-IO function that calls a function that does IO. EDIT: with the exception of doing IO on a freshly allocated, in-memory buffer that doesn't escape the function call.
- DarkUranium 6mo agoAlso with the exception of trace/debug print. At least I'd consider it a fair exception. (it's what I'm planning for my own lang, having `trace` be "blessed" to not make a function unpure despite I/O)
- kibwen 6mo ago> The functions have the same calling conventions regardless of IO implementation. Okay, then by that definition, Rust doesn't have colored functions either, because `async fn foo() -> Bar {` in Rust is just syntax sugar for `fn foo() -> impl Future<Output=Bar> {` (that's a normal function that returns a future), and `Future` is just a normal trait that provides a `poll` method; there's no different calling convention anywhere. So which is it? Either Rust and Zig both have colored functions, or neither do.
- satvikpendem 6mo agoOr use OCaml 5 which has a full algebraic effects system that solves the function coloring problem while still being highly performant.
- coldtea 6mo agoHow do they solve it?
- satvikpendem 6mo agoI'm glad you asked: https://lukstafi.github.io/curious-ocaml/new_book.html#chapter-9-algebraic-effects https://lukstafi.github.io/curious-ocaml/new_book.html#chapt... Basically, any function can handle any effect, they don't need to be marked a special way like async await.
- joshsegall 6mo agoAuthor here. You're right in the sense that some forms of function coloring have good tradeoffs, though I think that's a watered down version of that original meaning. The point is that async/await tracks the wrong thing in the wrong direction, not that "tracking effects in types is bad." In a proper effect system, subsumption goes the right way: pure is universally callable and handled effects disappear. Async/await inverts this: the "simple" color (sync) is the restricted one.