Colored functions represent exactly the failure state of types: When you've over-tuned your typing to the point that it takes substantial refactoring to make something that should just work, work.
Whether a function is called asynchronously or not should be a decision of the caller, not of the function itself. Sometimes it doesn't make sense for me to continue when I don't have an answer yet. Sometimes it does. That makes it the caller's concern.
The solution to this in Javascript would be to simply make every single function an async function, but that introduces a bunch of clutter because async/await is a feature to be bolted onto an existing language, rather than a paradigm to build your language around.
In contrast, look at Elixir: No function coloring to be found. Want to run something asynchonously?
res = Task.async(&any_function_i_like/0)
\\some other stuff
out = Task.await(res)
(that's syntax sugar for spawning a process that will die and produce a result at some point and then waiting to receiving a message from it). Want to run that same function synchronously?
out = any_function_i_like()
No colors, no nonsense.
"But wait!" you might be thinking "What happens if the task fails?" Well, that's up to the caller, too. Maybe that only rarely happens, and there's no logical path forward from that failure, say, a very tiny meteor punches out the processor core that task is running on. That's okay, by default if the child process dies, so does the caller. This is passing the buck, but is the correct answer more often than not, so it doesn't make sense to make the developer do extra work to make it happen. But maybe a failure is somewhat likely, and we just want to keep trying until it works. That's easy, too! Just spawn a supervisor process that'll make a new copy of the called function if it fails. One more line of code relative to the async case. Maybe the function is just to make some side effect, and it doesn't matter if the parent crashes or not. Maybe it does.
There's a lot of different cases when it comes to concurrent programming. It does not make sense make those decisions unilaterally for someone using your library. Leave it up to the caller to decide whether your function is synchronous, asynchronous, long-lived, independent, dependent, etc. Unfortunately, that's hard to tack onto a language as an afterthought, so we get Async/Await.
> The solution to this in Javascript would be to simply make every single function an async function, but that introduces a bunch of clutter because async/await is a feature to be bolted onto an existing language, rather than a paradigm to build your language around.
I completely disagree with this, and I think that's a strong summary of why our opinions are so hugely diverged here. Async/Await was decades of programming language research in the making. It's "just" Haskell's do notation but with a lot of smart research into "okay, but how do we do that for beginners and junior developers". It may seem like an overnight success, but that's not because it is "tacked on", it's because it is well thought out, well tested, and well designed and therefore has spread to a number of programming languages. (I didn't say anything about Javascript. Javascript wasn't the first to add async/await and wasn't the last.)
I'm not sure it is a "failure state" of types, but I can agree it is a hard edge case, and there have been criticisms of it for decades. The same "why isn't every function in Haskell in the IO monad and written in do-notation" is the same "why isn't every function async/await" complaint. It's the same reason not every function in a language with iterators is written as a generator with yield instead of return. It's the same reason you don't solve every problem with a single RegEx. It's the same reason you use OO classes for encapsulation (or don't). Pragmatic programming languages are never just a single paradigm.
The callee knows what resources it is encapsulating and which of those need to be asynchronous. It knows better than the caller if there is going to be a suspend point what state it will need to transition into next and can prepare its internal state machine best for how it needs to operate. It's useful to bubble up that library knowledge into the type system and async/await gives a strong type safe way to do that (plus eases a lot of the trivia of building a state machine in languages that support async/await). Just like a generator function is a basic state machine for iterable results. Just like an OO library might encapsulate some amount of memory usage.
Your elixir example is a single function and you can do that in any of the async/await libraries to. The trick to async/await is that what you are writing in async/await isn't a single function but a complex state machine. Have you actually tried to rewrite async/await-heavy code to something like Elixir? It's not hard, but it is a lot like hand converting a RegEx to a DFA.
That doesn't mean that caller's "lose power", the power dynamic is shifted, but it isn't as deranged as you seem to think it is. async/await just defines the state machine possibilities, it doesn't know or care if the async/await state machines that have been defined complete synchronously or asynchronously. It doesn't care what threads/threadpools/green threads/greenlets/threadlets/whatever else those state machines run on, including the same thread as the caller. The state machines are generally agnostic to how their state transitions are triggered. Javascript doesn't give you a lot of power there, but that's because browsers have always been in charge of JS threading and that was true before async/await. That's not a deficit of async/await, that's a deficit of Javascript. (That you can't see past Javascript may indicate why you think it is a tacked on part of the language and not a success story from other languages modestly applied.) You can look up some of the tools that C# presents, for example, to let callers influence downstream scheduling. You can look up all the complaints that Python despite being the "there should be one clear way to do it" language decided that instead "explicit is better than implicit" beat out as the winning mantra and left scheduling to a handful of different libraries with different opinions of how async/await should be scheduled by callers, but the benefit is that apps have to explicitly opt in to one of those behaviors rather than a "works fine for most developers" out of the box default. The "power" remains you just have to learn new ways to apply it.
Whether a function is called asynchronously or not should be a decision of the caller, not of the function itself. Sometimes it doesn't make sense for me to continue when I don't have an answer yet. Sometimes it does. That makes it the caller's concern.
The solution to this in Javascript would be to simply make every single function an async function, but that introduces a bunch of clutter because async/await is a feature to be bolted onto an existing language, rather than a paradigm to build your language around.
In contrast, look at Elixir: No function coloring to be found. Want to run something asynchonously?
(that's syntax sugar for spawning a process that will die and produce a result at some point and then waiting to receiving a message from it). Want to run that same function synchronously? No colors, no nonsense."But wait!" you might be thinking "What happens if the task fails?" Well, that's up to the caller, too. Maybe that only rarely happens, and there's no logical path forward from that failure, say, a very tiny meteor punches out the processor core that task is running on. That's okay, by default if the child process dies, so does the caller. This is passing the buck, but is the correct answer more often than not, so it doesn't make sense to make the developer do extra work to make it happen. But maybe a failure is somewhat likely, and we just want to keep trying until it works. That's easy, too! Just spawn a supervisor process that'll make a new copy of the called function if it fails. One more line of code relative to the async case. Maybe the function is just to make some side effect, and it doesn't matter if the parent crashes or not. Maybe it does.
There's a lot of different cases when it comes to concurrent programming. It does not make sense make those decisions unilaterally for someone using your library. Leave it up to the caller to decide whether your function is synchronous, asynchronous, long-lived, independent, dependent, etc. Unfortunately, that's hard to tack onto a language as an afterthought, so we get Async/Await.