I think the view of asynchronous functions as "upwardly infectious" comes from focusing too much on JavaScript.

Async/Await arguably[1] first debuted on C#, and in C# you could always call Task.Wait() or Task.Result to block on the Task returned from the code. Similarly, Python async coroutines can be run from blocking functions using event_loop.run_until_complete(coroutine). Kotlin similarly has runBlocking(). Running async code from blocking code is also possible on Rust, but it depends on your async runtime (tokio, async_std, etc.). JavaScript/TypeScript is the only mainstream language which completely disallows calling async from blocking code, and that is only due to its single-threaded (mostly) never-blocking design.

That doesn't mean you should always feel free to call asynchronous functions from synchronous ones. For instance, if you have a limited number of synchronous threads, this could be a bad idea. Likewise, if you call an async function that calls a synchronous function that then calls an asynchronous function again — you're likely in for trouble. So asynchronous functions are upwardly infectious in a way: You can call them from synchronous function, but in many (or even most) cases it is not a good idea to do so.

But wait! It's even worse: some synchronous functions can also be viewed as "upwardly infectious". Think for instance of a function that does synchronous network I/O: You can't call that function from an asynchronous function, without blocking (or even potentially deadlocking) one of your precious few scheduler threads. In fact, every function that does blocking IO is suspect, although some local blocking IO might be fast enough to ignore. FFI, Kernel thread synchronization and other interactions with the kernel are also danger territory.

So in practice, we never had just two colors to describe the synchronous properties of a function, but three:

Blue functions are neutral. They do not do any IO, or at least do not block on IO (they may do fire-and-forget IO or have other side-effects: we are not typing all IO effects here).

Yellow functions are blocking. They may perform IO, block on a kernel mutex, or even just send the kernel thread to sleep.

Red functions are called as if they are synchronous, but are transformed by the compiler to execute asynchronously and suspend. They may perform non-blocking IO or wait for asynchronous synchronization constructs.

All of these definitions are soft. Apart from JavaScript, colored languages are as strict very strict as they are imagined to be. You can directly call a yellow function or blue function from anywhere, and you can call a red functions from a blue or yellow function with very little extra ceremony. But instead of a strict set of axioms, we have a set of caveats for why you shouldn't do that:

The caveats are:

* In most language, blue and yellow functions are syntactically similar and there is no way to tell them apart, except for reading the function source or consulting its documentation.

* You can safely call blue functions from anywhere.

* You can safely call a red function from within a yellow function — but sometimes need to know the right scheduler event loop to run it on)

* You can safely call a yellow function from within a red function — but to avoid blocking and saturating your scheduler, you need to run the yellow function on a dedicated thread or a thread pool. Performance may suffer, so you want to avoid this kind of call as much as you can.

* You can call both yellow and red functions from within a blue function, but by doing that, but the function becomes yellow (but as mentioned before, this change is syntactically invisible). Blue functions become yellow even if you call a red function, since you'll be calling the red function from within a blocking helper (e.g. Kotlin's runBlocking() or Python's loop.run_until_complete()).

Looking at these caveats, even if we inverse take a "colored" language and inverse the default color from synchronous to asynchronous, we still end up with an even more nasty version of upward infection, since yellow functions are not even marked. This is not the same as the truly binary "pure/impure" distinction.

I think the real issues that prevent some languages from committing themselves to be fully colorless are:

1. The language must support a legacy blocking library functions or foreign interfaces (i.e. need to support "yellow functions").

2. The language wants to support pluggable or customizable scheduling.

The only languages I know that cab completely let go of these concerns are Go and (possibly) Erlang[2]. They have their own runtimes that covers every aspect of IO and synchronization, and strictly limit the ways in which you can do FFI[3] or customize their scheduler. Once you took care of that, you have no yellow functions, and you can ensure all your functions are the same color.

Languages like Rust, C++, C#, Kotlin and Swift need to deal with a legacy baggage of both synchronous blocking IO and previous attempts at asynchronous IO (like callbacks and future/promises). They cannot eliminate this baggage so easily, and the path that Go has chosen is just not open to them. So I believe having colored functions is an acceptable choice. In fact, I lament the choice to conflate blue and yellow functions to the same category, since calling blocking functions from within an async function is a very common mistake I see people doing.

What we really lack is a way to compose blue functions with red functions better by parametrizing blue functions so they can accept function arguments of any color.

[1] A pretty justifiable claim can be made that C# was inspired by F# asynchronous workflows - which also could use Async.RunSynchronously to run them.

[2] Loom does not completely do away with function colors in Java, unfortunately, and yellow functions still remain. For instance, any synchronized method or block is yellow and cannot be called from virtual threads. Since Java doesn't have red functions, the situation is worse in some regards — it's very hard to tell if the function you're writing will be running on a virtual thread and whether you can use yellow functions - which are themselves quite hard to discover. Hopefully, synchronized blocks will be slowly eliminated from the JVM and everything will be made compatible with virtual threads. But until then, Java is not a colorless language - it is just a language where function color is entirely implicit.

[3] https://twitter.com/pcwalton/status/1370132795557748740