7 ms·
I've always found the criticism leveled by the colored functions blog post a bit contrived. Yes, when you replace the words async/await with meaningless concept
by wesselbindt 2y ago
I've always found the criticism leveled by the colored functions blog post a bit contrived. Yes, when you replace the words async/await with meaningless concepts I do not care about such as color, it's very annoying to have to arbitrarily mark a function as blue or red. But when you're honest with yourself and interpret the word "async" as "expensive", or as "does network calls", it becomes clear that "async/await" makes important features of your function explicit, rather than implicit. Seeing "await" gives me more information about the function, without having to read the body of the function (and the ones it calls, and the ones they call, etc). That's a good thing.
There are serious drawbacks to async/await, and the red/blue blog post manages to list none of them.
I've wrote a blog in response to OP containing a more detailed version of the above:
https://wpbindt.github.io/async/opinions/programming/2024/01/21/red-code-blue-code-honesty.html https://wpbindt.github.io/async/opinions/programming/2024/01...
- klabb3 2y ago> But when you're honest with yourself and interpret the word "async" as "expensive", or as "does network calls", it becomes clear that "async/await" makes important features of your function explicit, rather than implicit. The main issue here is that whether or not a function is expensive is known upfront. In practice, this bites you when (a) you refactor (the impl changes), and (b) when you’re writing interfaces (the impl is unknown). This causes cumbersome cascading refactors and even complete loss of interop (in case where a 3p interface is sync and your impl is not). In order to argue against that point, you have to argue that async is upfront-predictable and obviously part of the interface. And that it should be considered a breaking change - or semantically different if you will.
- shadowgovt 2y agoKey to note here is that asynchronicity impacts interface because it's an entirely different kind of function in a way that "regular" functions are not. Regular functions always return (or crash the program). Regular functions run to completion before returning control to the caller. These are two key properties async functions lack, so the caller has to care. It's analogous to the notion that checked exceptions are challenging because they also become part of the interface and so the caller is impacted by implementation details. (This does raise the question of whether there's a form of async hiding just out of sight of current language designs that's analogous to "unchecked exceptions": can one construct a model of function calling that makes one not have to care about async vs. sync functions at the cost of something else? I suspect, without exploring too deeply, that Orc (https://orc.csres.utexas.edu/ https://orc.csres.utexas.edu/) has gone that road).
- klabb3 2y ago> it's an entirely different kind of function Absolutely. Async is sugar for state machines. There is nothing wrong with that per se. > Regular functions run to completion before returning control to the caller. Yep. However it's important to note that it’s very possible to cancel regular functions IO from a language perspective, using a cooperative signal. (I always squirm a little every time someone says cancelation is easier in async - you shouldn’t imo cancel by dropping a state machine on the floor). The best example of this is context.Context in Golang. But even Tokio has CancellationToken. That said, the real benefit of the state machine is known size. This is the main (and I would say only) meaningful advantage compared to a regular stack. In Golang, you have dynamic per-goroutine (green thread) stack, which is wasting memory compared to rust async.
- enragedcacti 2y ago> can one construct a model of function calling that makes one not have to care about async vs. sync functions at the cost of something else? If I understand it correctly this is technologically possible in Python and there are two main tradeoffs depending on how the event loop is modified. given the following: x = 0 y = 0 async def set_x(): global x x = await request_x() # returns 5 async def set_y() global y y = await request_y() # returns 10 def business_logic(): if x == 0: set_y() # magic synchronous call to async func print(x + y) async def do_business(): business_logic() async def main(): await asyncio.gather( set_x(), do_business()) What is the value that's printed? If all coroutines share the same event loop then we don't know because set_y() could yield execution and give it to set_x(). The author of business_logic() may not even know they are writing async code because it just works, and the author of main() assumes that do_business() is concurrency safe because it's async. The alternative is nested event loops, so everything in business_logic() gets its own event loop that must fully complete before anything else in the parent event loop can run. This solves the concurrency bug but we've introduced blocking into code that was obviously intended to be async. This approach works (see nest-asyncio package) but the core devs have opted against it because they feel it makes it too easy to break the value of async on accident or out of laziness. IMO something that could work is a `sync def` declaration where the function is written in async style but gets its own event loop and can be called synchronously. It solves the tainting issue while still being explicit, easy to convert to full async, and easy to lint for in projects where it would be a net negative.
- spinningslate 2y agothis is a controversial topic - right up there with static vs dynamic typing. I found your post helpful and well written, but in the end I still find myself supporting Bob's blue/red delineation as a helpful articulation of the key limitations with async/await. Why? Because the only thing that the async keyword objectively says is "someone, somewhere down the call stack, decided to make their function async". You mention I/O in your post and it might indeed be because of that. You also mention cheap vs expensive and, again, it could be that. But those are heuristics: not objective or definitive. The cheap vs expensive point is particularly problematic (a) because there's no delineation between the two, and (b) because it's different for different use cases. Is reading a file expensive? It might be. If I need to read a 1GB file just to pick out 10 characters, then the read is expensive. But if I'm reading that same file to run a stochastic simulation, then the time to read the file might well be insignificant in comparison to the computation. And, besides, I can't run the computation until I've read the input data anyway - so it's not helpful to me that someone else decided that reading files is "expensive". The other argument I've seen for marking IO as async is partial failure: IO can fail in ways that local computation doesn't (modulo hardware failures - which can occur, though are much less common). That would be legitimate - except, per above, "async" doesn't definitely mean "does I/O". It only means someone, somewhere in the chain, decided to make their function async. And now I'm constrained to make my functions async too because of its transitive nature. I'm not against cooperative multi-tasking per se. I do prefer the developer experience of runtime-supported fine threading such as Erlang processes or goroutines. That's a preference though, and others will feel differently - ref comment on static vs dynamic typing.
- jayd16 2y ago> It only means someone, somewhere in the chain, decided to make their function async Well it specifically means that a function can yield its execution context. That property is taken on when one function calls an async function. It has clear implications for very common threading patterns, I think it's right to argue that using blue/red obfuscates that.
- shadowgovt 2y ago
- enragedcacti 2y ago> Yes, when you replace the words async/await with meaningless concepts I do not care about such as color My issue is that in some contexts the cost of the function is effectively meaningless. A lot of python scripting falls into this category, where performance is not a concern or there is nothing useful to accomplish in the process while waiting for I/O. At that point any async up the stack is just forcing the user to deal with implementation details they shouldn't need to care about. I can appreciate that it enforces good design decisions in some contexts, but marking that up as a pro feels like tasking the interpreter with doing the job of a linter.
- pxeger1 2y agoAnother key thing is that in many languages, you can (and will often enough in practice for it to be a concern) do I/O outside of an async function, using the equivalent blocking API. So in those languages, async does not act as a useful “this function is expensive/impure” marker.
- colejohnson66 2y agoThat's because many languages start without async/await. So, sync overloads stay for eternity for backwards compatibility. If I could design a language from scratch, I'd argue for async/await OOTB. For example, the Microsoft Midori team did "async everything" in their C# derivative.[0] [0]: https://joeduffyblog.com/2015/11/19/asynchronous-everything/ https://joeduffyblog.com/2015/11/19/asynchronous-everything/
- jayd16 2y agoReplace "expensive" with "this function can yield" and your argument becomes much stronger. The peer replies here are dinging you on the vagueness of "expensive" but it's not really about that. The explicit information given by async is whether a function will yield its execution context or retain control of it. If you're leveraging "has control of the thread" as a form of concurrency (eg the UI thread) then knowing a function is sync or async is extremely important.
- Calavar 2y agoI think you read the article as saying that juggling function color is trivial nonsense and a waste of programmers' time, and your rebuttal is "await" isn't trivial - it conveys important information about the function. My reading of the article is very different. I think the main thrust is function color is infectious, and that's an issue because adding "async" to a single function can have cascading effects through your codebase, which makes refactoring difficult and slows down development speed. This is true even if "async" conveys important information.
- eyelidlessness 2y agoThis. Plus, the cascading effect isn’t necessarily just difficult to account for, sometimes it makes integration effectively impossible. As an example, a project I work on provides functionality implementing arbitrary computation graphs, with an explicit guarantee that all computations are synchronous, consistent and complete on return from every state change call. This guarantee is a conscious design decision that characterizes both the project itself (and its API boundaries) as well as many of its internals predicated on the same principles. It does explicitly account for asynchronous behavior at the entry point, and at very specific exit points. But introducing async anywhere else in between would mean building a fundamentally different project, and probably revisiting business goals in the process. The function color problem isn’t just annoying or tedious, it can be fundamental to whether certain projects are even viable.
- fwip 2y agoTo me, this actually reads as a reason for function coloring. Your project makes guarantees that neatly match up to the division between async and sync code, and it would be fundamentally incorrect to ever call a function marked as async from most points of your program flow. So in this case, it would be good that the language enforces this for you, right?
- assbuttbuttass 2y ago> it would be fundamentally incorrect to ever call a function marked as async from most points of your program flow For a language with async/await yes, you need to bifurcate your program into synchronous and asynchronous pieces, and be very careful to maintain that split. For a language with a synchronous thread abstraction like go, you simply don't need to create this artificial bifurcation in the first place
- gpderetta 2y agoThere are two issues with async/await annotations in most languages: - The annotation doesn't guarantee any particularly useful property: in many languages async functions can still block or take arbitrary time, while on the other hand non-async functions can still lead to reentrancy issues. - Most languages do not allow abstracting over async functions: you end up having to write the same higher order function twice even if it would be agnostic over the effectfulness of its parameter. Some languages, for example some of those with first class effects and no implicit side effects, you can both enforce the property and abstract over the effectfulness. But for most languages the real reason the async annotations exists is because it is a way to syntactically enforce that only a single stack frame can be suspended as opposed to a whole stack, which is tradeoff between usability, and performance and space usage. The supposed benefit is a post-hoc rationalization.