What Color is Your Function? You Better Know!
I never got around to commenting on this somewhat notorious post What Color is Your Function, by Bob Nystrom. This was from 2015 so I feel a little guilty about only ranting on it now — but it continues to get traffic and is quite wrong-headed.
In any case, the post is your basic “old fashioned programming language rant” (his words). The post is structured as a hypothetical where you are designing a new programming language. The language has the characteristic that each function has to have a color (red or blue). On top of this strange requirement, there are all these “arbitrary” restrictions on how you invoke these functions and how (or whether) you call one color function from another of a different color. What a crazy way to design a language!
Of course, in reality “red” and “blue” correspond to “async” functions and “sync” functions. And these constraints and requirements correspond to real constraints in most of the languages we actually use.
The whole post (and the related Hacker News thread that was dominated by programming language folks) fails to consider that you are actually writing programs that interact with the real world. The difference between sync and async is not just a random “aspect”; it is fundamental to the world and fundamental to the way you (should) structure your entire system. Sure, you can ignore it for some command line tool (like a compiler), but almost any long running application (like an app with a user in front of it or a server managing lots of requests) needs to fundamentally understand when it is invoking something that is fast and local (typically sync) and when it is invoking something that is potentially long-running and slow.
Crucially, that long-running thing will have different error characteristics, will introduce need for timeout control and cancellation capability and will also introduce concerns for how to manage internal program resources and state during the period of execution. You virtually always need to hold on to some resources — at least memory — to be ready to process the response when it finally arrives. In fact one of the most common ways that services “blow up” is when they are holding on to lots of resources and some queue of requests get stalled behind a non-responding dependent service. You usually need to do explicit throttling around how you manage queues and make requests between different components of your system.
In a graphical application (browser or native), sync vs. async is absolutely fundamental to how you manage state. At any given time, you need to provide a consistent view of the application model for the user to interact with. Ignoring that you just called a function that could take arbitrarily long to complete (or timeout) is called a “hang”.
In fact, we have experience with a system with one color — Win32. Virtually all functions except for a very few low-level routines were designed to be synchronous. It was generally left as an exercise for you as the programmer to figure out how to “wrap a thread” around an API call to keep your application from hanging. There was no red/blue signature — you had no idea whether the routine you were calling was going to take milliseconds, seconds or minutes to complete. This played a fundamental part in the horrendously common hanging behavior of Windows applications. It also played a role in the excessive thread and resource consumption of Windows apps (using threads as an abstraction for waiting rather than an abstraction for computing).
In fact, I would argue (have argued) that the explicit characteristic of asynchronous APIs has played a significant role in improving application responsiveness over time.
It is certainly possible to design systems where you only have synchronous functions and use message passing and multiple threads of execution (the long-running Midori research system at Microsoft is one example of a system that worked this way). This might make language designers happy but it doesn’t actually simplify the intellectual effort necessary to structure the overall system. You still need to think about how these messages (or other thread rendezvous events) get processed as an asynchronous event to update the stable state of the system. In a graphical application, you often have even more challenging issues around both exposing the intermediate state of some asynchronous computation (e.g. you are laying out the pages of a long document and would like to show the current page even if layout is not complete), or having user actions further impact an ongoing computation (more editing makes the current layout processing out-of-date so you would like to cancel and restart).
In practice, it is often the case that one “large” asynchronous operation is composed of a number of smaller asynchronous steps and it may be possible to use techniques like async/await and promises to write parts of the computation in a form that looks generally synchronous. But you still cannot ignore these larger system design issues. Asking why you would need to know the “implementation detail” that a function you are calling needs to invoke an async function internally is like asking why you need to know that some function you are calling can hang your app. It seems pretty clear why you need to know that.
You’re better off if you “revel in the asynchrony” — and actually address the higher level system design issues — rather than treat it as an annoying statement or function-level issue to be papered over by programming language syntax.