Rust’s async isn’t colored
hobofan.com
hobofan.com
> One the one hand it's a great article that hits the nail on the head why using async/await in JS (and other languages) is very painful
> Rust's async/await even though - you might have guessed it - it doesn't apply!
So the author starts off by declaring that the colored functions problem only applies to other languages but not to rust.
Soon after, he takes exception with the claim "You can only call a red function from within another red function".
His primary argument in support of this is that in rust you can execute an async function blockingly in an executor. And to do vice versa, i.e. call a sync function in an async loop, that you can either block the event loop or hand it off to a different machinery.
The fact is, both of these things can be done in other async runtimes as well. For eg. it's a near identical scenario in python async. Like others have pointed out in this comment thread, this is true in Scala too. Perhaps the author is unaware of this?
So yes, rust does indeed have the colored function dichotomy, as any async runtime would, the author's angry assertions to the contrary notwithstanding. (I say this as a fan of both rust as well as async runtimes, which I feel are a nice fit for certain use cases.)
``` async function main() { await stuff(); }
if (module === require.main) main(); ```
The await/async feature seems like a sensible addition to JavaScript, but I'm open to the idea that there are alternative solutions that would be even better. As an example, I don't know much about what Java is up to but I understand it's not going with await/async exactly.
It might keep things simple, but I don't know if this is really a sensible design constraint going into the future. Parallelism is where performance is going to come from in the future, and having the web - the most common way most people consume software - be single-threaded seems like a massive waste of hardware.
If I have a 16 core processor just so the add trackers in each of my chrome tabs can run in parallel, this is a world I don't want to live in.
JS being single threaded means that while some tracker is parsing a big json blob your website becomes unable to handle input events until the tracker is done.
Let’s say there is an option to do this in some form of threaded worker or coroutines then the main UI thread does not become blocked.
I agree that it's a neat idea to use asynchrony to move specific heavyweight browser-native computations out of the JavaScript thread, but is JSON parsing really a significant performance issue?
> JS being single threaded means that while some tracker is parsing a big json blob your website becomes unable to handle input events until the tracker is done.
I'd rather parallelism not be a viable option for accelerating trackers. Users are known to hate unresponsive UIs. I'd rather that web trackers bring an unavoidable performance penalty.
> Let’s say there is an option to do this in some form of threaded worker or coroutines then the main UI thread does not become blocked.
You've just described the Web Workers API.
I don't know whether today's tracker scripts use Web Workers. I imagine probably not though, I think the Web Workers API is only appropriate when you're doing substantial computation.
JS parsers, compilers, and even some garbage collectors are threaded.
IO (arguably the most common case for threads) is threaded behind the scenes.
Web Workers allow using multiple cores/processes for typical parallel programming. Message passing exists as do shared memory and even atomics (note: node, firefox, and chromium-based browsers all have it available after Spectre/meltdown mitigations were added, but it's still disabled in Safari).
What JS really needs is a CSP or Actor model on top to abstract away threads/OS processes/cores/hardware and let the VM handle them seamlessly like in Erlang/BEAM.
What kind of website would benefit from this?
If it's concurrency, is it something that could be done in a library?
As for parallelism, I don't think many SPAs and PWAs really need to be able to make full use of multicore for acceptable performance, do they? If you're writing heavyweight number-crunching code, that belongs either server-side or in a native application.
If you try to port Red Dead Redemption 2 to JavaScript, you're going to have a bad time. That's not the fault of the web, it's just not an appropriate choice of platform.
Like I said in another comment in this thread, the downsides of the web as a platform wouldn't go away by improving its support for parallelism or for concurrency. Web-based UIs would still seem clunky and half-baked compared to native apps, and they'd still use far more computational resources than native apps.
I also do prefer native, yet as mentioned, web browsers or web widgets as GUI toolkits are a fact and aren't going away.
Specially if everyone keeps pushing packaged ChromeOS apps, aka Electron.
Others have already pointed out I wasn't strictly correct when I put there's exactly one thread, on account of the Web Workers API. You can write parallel code for the web if you really need to, but it's somewhat walled off from the main JavaScript environment, so we still get to keep much of the benefit of the single-threaded model. For instance, all UI events are handled in the main thread, and that's not something you're empowered to break or override. That's a good thing.
Really though, if your page can't get by on one core of a modern CPU, it probably shouldn't be based on web technologies at all. Ordinary websites have no business writing parallel code just to prepare the DOM. I'd rather the average site not have the option at all. The web will never be the ideal platform for writing parallelised number-crunching code like modern high-performance game-engines, neither should it aim to be so.
> If I have a 16 core processor just so the add trackers in each of my chrome tabs can run in parallel, this is a world I don't want to live in.
I don't follow. What can't you currently do with that enormous computational horsepower? The browser already has plenty of power. Spam and bloat aren't going to go away from the web any time soon, and anyway they're an argument against adding even more features into the browser.
The downsides of the web as a platform wouldn't go away by improving JavaScript's ability to execute code in parallel. Web-based UIs would still seem clunky and half-baked compared to native apps, and they'd still use far more computational resources than native apps.
Same problem with Go's `Context` or error as a return value.
Same problem with pure/impure functions, or functions returning optional, or platform-specific functions, etc.
Sync/async is not special, it is just one instance of a problem that comes up pretty much everywhere (Algebraic effects may be a solution, but I'm not sure).
E.g. let f call g which calls h. And suppose you replace h with an async function. Then you'd have to manually annotate g and f as async functions too. You don't have to annotate a function as being allocating or not, so the allocation "color" isn't nearly as bad. Languages like Go or Julia or Zig that have "colorless" coroutines are much more robust.
And suppose you replace h with an allocating function, then in Zig you have to manually add a allocator as a parameter in g and f and update their call site accordingly, because now they are also allocating.
And suppose you replace h with a function that returns an error, then in Go you have to manually add an error as a return value in g and f.
h become impure? Then g and f are not pure anymore either.
h becomes Windows specific, then g and f are Windows specific[1].
The situation is highly similar.
[1]: or you need to write a non-windows version of the work done by the h function, which fundamentally equivalent to calling block_on on an async function to make it a blocking one.
This is not entirely correct analogy. You can embed your allocator inside one of the parameters, too, as a strategy (perhaps a certain type of "object" should carry it's allocator with it).
Then uncolored generic functions could operate on allocator-bound and allocator-unbound "objects" alike, even if say the generic function calls a "object method" that performs an allocation in one case but not the other.... You just have to be careful about how to handle errors.
There is no equivalent for async.
If you were going to be serious about function coloring in zig, the place where there is function coloring is functions with error return values but that's actually a reasonable place to have function coloring, and still, it's easy to build seams in both directions, so a red function can call a blue function, and a blue function can call a red function pretty easily.
Sure there is, a symmetrical one: you can embed the Future/Promise inside one of the return value of you sync function. Both work, but both are equally hackish.
> If you were going to be serious about function coloring in zig, the place where there is function coloring is functions with error return values
I actually address it a bit below.
If you make code that requires allocation and does not have an allocator passed into it, there is literally nothing stopping you from allocating anyway. Zig does not stop you from, for example, heap allocating without the usage of an allocator (Zig does not know what heap allocation even is). You can directly use the std page_allocator (and others) wherever you want, too.
There are very few good reasons to do this (e.g. spawning a thread requires particular allocation; using an allocator might not be sound depending on the OS).
All you have to do is document that it allocates memory and that's fine; there is no split.
But nonetheless I think there is a split (at least for now). Zig async function frames are different than regular functions, so even if syntactically, `foo()` could magically be switched between sync or async based on io_mode, in practice if you have a recursive call tree doing fs operations, it'll work in one mode (up until a stack overflow, that is) and throw a compile error upfront in the other. Presumably this would not be an issue anymore once [0] is implemented.
I'm most familiar with Go, and there I've gotten very used to the idea that my functions will probably need an error return, so e.g. I write my main func to call into a run(...) error func first thing. I've had to go back and replumb things with context, and it's no fun. Similar strategies apply in JavaScript, where you just know that any kind of button press will probably need to be a Promise because it might have to wait on a network call or whatever. It's a pervasive problem.
Zig has the same codebase with regards to allocation as well; there is no "allocator_std" or some nonsense like that.
Where is the paradox, exactly?
It depends on what you mean by "separate codebases." asyc_std is a third party package someone made, so it's a separate codebase because of that.
We could also include all of it (or something like it) and it would be a "single codebase," but I think that's more about your wording than your point.
As I said in the original comment, sync/async and no-alloc/alloc or no-error/error are in a parallel situation. And in a language design you can either expose “colors” to your users, or hide it through some magic and a global state. I personally prefer the former, and I liked Zig's idea to expose the allocation explicitly, instead of what Rust did initially (there are slowly rolling out the option to use a specific allocator for collections, which is good even if it took almost 6 years after 1.0). With error Handling, Zig also took the same route with error sets instead of exceptions.
That's why I don't think the colorblind async mechanism is a good fit in the language (and it adds some cognitive burden, because newcomers need to learn that looking at code full of async/await doesn't necessarily mean they are looking at non-blocking code).
(Also, async-std is a third party library with little in common with std and whose name has been chosen for “marketing” reason, so I' don't really think the comparison is relevant.)
> For eg. it's a near identical scenario in python async.
I'm at no point claiming that Python is colored (the "What Color is Your Function?" post does). It seems that by my interpretation Python may indeed be as uncolored as Rust (I've only used it in a very limited fashion). Can't compare Scala as I haven't used it.
From the original post:
> This is where the “red functions can only be called by red functions” rule comes from. You have to closurify the entire callstack all the way back to main() or the event handler.
I think that very much supports my point, and I don't see why async runtimes should not be allowed as a tool to call a async from a sync function. In some languages (like JS) that have their own runtimes, you don't really have that choice though.
Rust assists you by enforcing the distinction with compile time type checking. But all that is doing is yelling at you that you are trying to mix the two fundamentally non-composable things together. You still have to do the conversion yourself. You still have to do extra work to accomplish the conversion.
Also it's just ugly to have layer after layer of async/sync wrapping going on. So what ends up happening is that you end up preferring one color over the other. Either you try to keep everything asynchronous or synchronous to avoid the whole thing.
I disagree with this take; I understand the point of the original “What color is your function?” as colored functions creating an impassable divide between Promise-wrapped values and sync code.
Indeed, in the end, the author mentions e.g. C# as not being colored, because even if sync/async functions are different, it features escape gates to unwrap promises and bring them back into the sync world.
> trying to mix the two fundamentally non-composable things together. You still have to do the conversion yourself.
I think the fact that you can do the conversion shows that they are composable. As I've written in the blog post, I don't mind verbosity verbosity much, as long as it gets me some control.
> The point of the colored function is that the color is a leaky abstraction.
And I think that's where the difference lies. In JS, an async function will leak all up your callstack, while in contrast in Rust, you can clearly control where the leaking stops.
> Also it's just ugly to have layer after layer of async/sync wrapping going on. So what ends up happening is that you end up preferring one color over the other. Either you try to keep everything asynchronous or synchronous to avoid the whole thing.
I think that's fair, but I haven't really had much problems with that in practice.
Go. Go eliminated the distinction. Everyone of his points about JS/C#/Dart all tick the same boxes he calls out as problematic in Rust as well. The size of the problem may be simpler but you haven't eliminated it.
Everything is composable if you wrap it appropriately. Saying that since you can convert something into a newer more composable form doesn't imply that the original is composable. It just means that since we can write code everything is composable with the right amount of work. What people actually mean when they say two things are composable is that you don't need a shim to compose them together.
Did JS Solve(tm) "colored" functions?
No, Promises can only be interacted with within the async framework. There is no ways to
1. Call an async function/get a Promise in a sync function
and
2. “Extract” the value in the promise and use it as naked value.
JS would need e.g. a “stop there, wait for the promise to result, unwrap its content and give it to me” instruction to be like Rust in this case.
For instance, this will essentially halt your runtime:
let y;
Promise.resolve(123).then(x => { y = x; });
while (!y) {}
console.log(y)This is true for most languages with async/await, and if you call the two colours differently and to get the full benefit you shouldn't mix them even though with some effort you could, because mixing them has a cost that's high enough to require two distinct sets of API, then guess what? You have coloured functions!
For an article that shows that async/await -- in Rust as in almost any other language (some languages employ the terminology async/await for a different feature) -- is very much coloured, the exasperation at something that is obviously true is perplexing. Perhaps the author believes that function colouring is not a big hindrance for them in Rust, or that function colouring is not a significant downside of async/await in general, but it is very much there. It's an odd title for an article that says, async is definitely coloured, but I can live with it.
Every language where you can call an async function as if it was blocking has no colored functions, and none of the issues of it.
There will be no issue in terms of program correctness, but there will be one in terms of performance assumptions. And performance might actually have been why the programmer chose Rust in the first place?
Seems like Rust's async are not correctness-colored, but they are performance-colored.
there is if it leads to deadlocks.
I think you've made a good point, though I disagree on the expensiveness. I think the point I should have tried to make is "Rust's async fns are colored, but I don't think that's a big problem".
In my head I used "suffers from the coloring problem" (which I would 90% attribute to point 3 in the post) and "is colored" interchangeably, but obviously there is a difference between the two.
In case of Rust the syntax difference is a deliberate design choice, because `.await` points can also return from the function. Rust handles errors with locally explicit `?` for the same reason.
It seems like a more concrete example of JS in contrast to Rust could have cleared up the confusion, but I didn't really want to make it a X vs. Y article that just comes of as language bashing.
So then C# "has no colored functions" despite being the prime example of them, just because there are escape hatches from "async await" in the form of .Result and co?
This seems like an absurd position to take.
Quite the opposite, the main target of the original post (https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...) is JS, not C#; which is explicitly described as not having the problem: “In C#, you can use the await keyword to invoke an asynchronous function.”
> So JS, Dart, C#, and Python have this problem
The reason why C# _might_ be considered to not have coloured functions is not because "you can use the await keyword to invoke an asynchronous function", it is that, when for whatever reason you cannot use `await`, then there are ways to synchronously call the async code, the simplest being appending `.Result` to the end.
Of course these are not ideal to use, but "escape hatches" to the coloured functions do exist; and yet there are still two "colours" to the functions" - async and not.
it is wrong to say that "C# does not have coloured functions". OK, you can work around this problem. But it's still a problem.
println!("{}", futures::executor::block_on(library::read()).unwrap());
https://github.com/MaulingMonkey/untokio/blob/892fab053b4f6d... var foo = [0];
function setFoo() {
console.log("Callback");
foo[0] = 1;
}
setTimeout(setFoo, 100);
console.log("Waiting..");
while (!foo[0]);
console.log("Done.");
Edited the original message with reversed arguments to setTimeout; doesn't seem to affect the results though. Really need to love the complete lack of error messages though..That's pretty much identical between IO, setTimeout/setInterval and requestAnimationFrame
I was confused in the beginning
You still need to manually manage color (aka blocking vs non-blocking, sync vs async). E.g., it impacts how you write the calls. So, the coder must be aware of the colors -- a big chunk of the problem / annoyance even if compilers / type systems can perhaps flag / catch errors.
I think what happened here was that the author found such awareness and management personally easy enough to use that he wanted to dismiss the entire coloring complaint. Then, in his zeal for dismissal, he made logical errors / mischaracterizations / exaggeration. It happens. Probably best to just move on. (EDIT: Focusing on such situations doesn't help perceptions of programming language evangelism/propaganda.)
Rust’s futures library actually contains a defense mechanism against precisely this behavior: a function “futures::executor::enter” that returns a guard that causes an immediate panic if another “futures::executor::enter” call is made on the same thread while it is alive. The idea is that every executor calls this when it takes over execution on a thread, so you get an immediate panic if you try to block on a future inside another future instead of risking a deadlock.
Here’s a specific scenario to think about. If I have synchronous functions `f`, `g` and `h`, `f` can call `g(h)` and `g` can in turn call its parameter `h` - no problems. Now if I have async functions `f2` and `h2`, can `f2` call `g(h2)` without the need to write an async version of `g`?
In a language like Lua (with stackful coroutines), the answer is “yes” - `g` can be called from and can call functions of either colour. But AFAIK in JavaScript (with callbacks, promises and async/await based on promises), and in Python (with async/await based on stackless coroutines), the answer is “no” - the colour of `g` must match that of its caller and callee.
What’s the answer for Rust? Can we answer “yes” by following this article’s recommendations of “spawn [g] as a blocking task” and “throw [h2] on an executor“?
f2(g(h2()))
you'd write something like f2(async { g(h2().await) }).await
if f2 takes a future, or f2(g(h2().await)).await
if it takes a value. (The latter is more common.) The equivalent in JS would be await f2(g(await h2()))If calling `h2` requires an `await` if and only if `h2` is async, then the function `g` can’t be used for both the synchronous and async cases. AFAIK this is the main objection to having two function colours.
function giveMe3() { return 3; }
function giveMe3Async() { return Promise.resolve(3); }
async function logger(func) { return console.log(await func()); }
await logger(giveMe3);
await logger(giveMe3Async);I think this is similar to what the author is describing, in which case, I'd say that function color contagion is a feature of continuation passing style programming, and its sugared variants (futures, async/await), rather than being something that you can say absolutely about a language or a runtime.
I think it's generally less of a problem in Scala though because most libraries/frameworks etc are async by default which makes sense. It's much easier to take a sync effect and fit it into an async process (just wrap the result in Future.successful/IO.pure/etc) than it is to fit an async call into a sync process (you can always do Await.result/IO.unsafeRunSync but it can have some very perverse effects on runtime behavior).
As far as I can tell, to get a value out of an async function, you use block_on() if called from a regular function, but .await if called from another async one.
That's an example of function coloring, and it does not have to be this way (see eg Raku - formerly known as Perl6 - and, if I'm not mistaken, Julia as well[1]).
Now, you might argue that it is an innocuous example of function coloring that does not come with the negative implications of Javascript's version, but that doesn't mean the coloring isn't there.
[1] even in these examples, there's still a bit of colouring going on insofar that there's a difference between returning promises or values which Zig avoids, but their approach looks a bit too action-at-a-distancy for my taste...
What's the point of using a type system if "color" is not a feature?
It eases maintenance and working as a team.
And regarding complains about requiring to add or removing await everywhere, this is equivalent to saying "you shouldn't tell everyone that your proc can suspend and all assumptions about the real control flow are off".
What it speaks about a "coloring" of the call context!
BLUE => Blocking I/O call context.
RED => Async I/O call context.
And calling a function which expects a blocking I/O call context in a function running under a Async I/O call context (or the other way around) is sub-optimal.
Depending on the language this can cause anything from it works fine over performance problems and increased likelihood of dead locks to compiler errors or even super nasty runtime bugs.
The important part is it's 100% language depended. But people use arguments like "that's colored so it's bad" (because of the color) or "that has a different color so you can't call it here" (because of the color). Even through it might very well not be bad that it's colored and you might very well be perfectly fine calling that there even through it has a different color then the calling function.
I.e. people use the argument in the same way people argue that static typing is fundamentally bad because there is some language where it brings more drawbacks then benefits.
There's a worse secondary effect of mixing async and sync code by using executors (async->sync) or posting to the async loop (sync->async). The first effect: it means that your sync functions that were previously non-blocking are now blocking, so all async callers are now forced to make calls into them with executors.
Even worse, if you have chains of async -> executor -> sync -> post -> async -> executor -> sync (as you might do by mechanically converting sync code), each executor call takes up an entire worker in the pool, and the Python thread pool has a maximum number of workers. You need as many workers as the sum of the maximum number of outstanding async->sync transition stacks in all of your async tasks. The penalty for underestimating this is deadlock.
It may be possible to come up with some more elegant solution to the overall issue by adding linearity to the Rust type system, but clearly this comes with its own issues wrt. ergonomics. All of these are really complex, research-level problems, and it's not clear whether they can be addressed sufficiently in an overtly "practical" PL design.
You have to "add colors", e.g. special map()s or wraps.
A Result<a, err> cannot be chained with a pure fn and an Option<a> without adding colors. Or a fn that throws cannot be called from a pure fn that never throws, without adding colors.
Js promises do wrap try/catch in most cases, but I recall some unusual constructor that doesn't.
Rust doesn't have exceptions, so Result<> is standard. It's painless to mix .map() with .and_then(), however I do think this is a case of "coloring". You also have to "color" when chaining with Option<>, which sometimes is conceptually between a pure fn and a Result fn.
The original article's key point was about async forcing refactoring of all functions in the entire call graph just because a leaf function needed it. This doesn't apply to async or options in Rust, because Rust functions can use these features without exposing that fact to their callers.
There is no single Monad type from which you can build and chain, you always have to color some calls.
You can implement a trait that lets you call both sync and async functions in the same way. You can with the same code and same syntax, get results of either `Fn()->T` or `Fn()->Future<Output=T>`.
I guess I was just going off some tangent.
Does anyone remember Aspect Oriented Programming? Past the peak of OOP excitement, there was a set of problems that OOP hadn't just made magically simple, and aspects were the solution. I recall it was always the same basic three examples that were used to illustrate why everyone needed them. Compilers were enhanced, libraries were written, lots of papers submitted, lots of conference sessions and talks. And then it all kind of faded away.
Is the async movement going to be a case of history rhyming with itself? I see some similarities (and of course some differences).
(If iOS Grand Central Dispatch is to be considered an async implementation, I consume a bit of async that way I guess)
"Throw it on an executor" doesn't change the fact that you had to change the way you evaluate different colored functions. So I guess it's still colored?
(How are function "colors" not just monads without saying "monads"?
Maybe if you understand function coloring you already understand what monads are and just don't know it yet.)
Doesn't the whole crate get colored, though, because now it requires an async runtime? Or is it possible to synchronously run an async function without a runtime? Genuinely asking; I haven't used this stuff yet firsthand
However, you can "un-color" them like this:
doAsyncThing
|> Async.RunSynchronouslyThrough as async in rust is implemented in libraries (just the async=>Future transformation + some shared traits/utility is provided by rust-lang) you need to import `block_on`. E.g. `smol::block_on(asyncFunction())`.
Rust has operator overloading, so the + operator may call a function, I don't call that explicit.
For me the 2 first checkmarks of being coloured are what matter most, the author argues that you can work around the third point, but it seems like a huge pain to do routinely.
I'm still in the camp that non colored async runtimes are more useful.
AFAIK the main objection to this model is that you have to write your code so that it can handle being suspended at any point. OTOH when using async-await, the only potential suspension points are the places marked with `await`.
Trivially you can call any function that can block. Less trivially, any function that you can can resume any other suspended coroutine. So in practice there isn't much you can rely on. In general you can not rely on state that has escaped your function to be unmodified across function calls (although here of course rust has tools to prevent that, but they would work even without the sync/async distinction).
It is true that other code can only run at function call boundaries, but that's true for any cooperatively scheduled runtime, even with stackfull coroutines.
I think this has proven itself to be the right decision. It's one of the main reasons that Rust works so well alongside other languages—there's very little default Rust runtime that complicates interop. There are also lots of programming scenarios for which green threads are not the right tool, and Rust accommodates those.
Through there are mono colored languages.
E.g. go would be mono red.
Normally mono blue languages allow you to "emulate" red contexts using callback's, listeners and similar. So some would argue there are no mono blue languages.
Many of the points (like red being harder to use) are 100% language impl. dependent.
The only think which is generally true about coloring is that mixing colors is a bit harder and sometimes can be sub-optimal for performance. But how much harder depends fully on your programming language and use-case might be negligible. Same is true for performance.
Also the two big problem of mono red languages is that while red has less overhead when a lot of parallelism is involved it has more overhaed if little parallelism is involved. And that interacting with other programs using a FFI conceptually harder. Which can lead to a varying degree of complexity and potentially performance drawbacks. But again how much this matters is again very language and use-case dependent.
All kind of tooling (including type checking) can reduce the drawbacks of either model (but introduces new drawbacks like longer compilation times).
As a side note: Communicating between red and blue functions is only potentially hard when they call each other. In the same way mono-red languages have only problems with external programs if they communicating over a FFI. If things like futures, streams, channels or similar are used (assuming proper implementation) red and blue code can normally communicate just fine, even if run in the same process.
well, that's exactly what is done in traditional C, C++, Java...
Not quite right.
Rust allows you to implement the + operator for your type, but not to overload it for other types. It furthermore avoids many of the problems other languages (C++) have with it by being more restricted in how you can implement it and strongly discourages any "unusual" usage of it.
Lastly there isn't rally any magical about `+` (in difference to some other languages). So `a + b` is just basically the same as `a.+(b)` except that rust doesn't allow symbols as function names so it's `a.add(b)`.
So arguing that because rust allows you to implement +/-/etc. it's not explicit is a very flawed argument.
Now you could argue that because of how rust's type system works it's not explicit. E.g. does `a.foo(b)` com from a trait implemented for A or is it a method on A? But here rust rejects compilation if it's ambiguous. So it's not a place where implicit creates a problem at all.
> I'm still in the camp that non colored async runtimes are more useful.
I would argue there are no non-colored async runtimes. It simply can't exist as far as I can tell. At best you can:
- Make a language which is mono colored, e.g. all blue or all red.
- Make a language which chooses colors for you.
But in both cases coloring still exists and you will still run into it when e.g. interacting with a FFI.
Also mono colored languages mean you have to use that color.
The runtime works everything out at the expression level to handle promises so developers don't need to deal with them:
In particular this was prompted by the article from yesterday[0], that in its opening paragraph blindly asserts that the coloring problems apply to Rust.
[0]: https://theta.eu.org/2021/03/08/async-rust-2.html discussed at https://news.ycombinator.com/item?id=26406989
An async function call is a massively different abstraction than a sync function call ... think about what logic is allowed to live on the other side of the async function call -- it can be literally anything without changing the caller ... sync function calls imply a very different set of expectations exist between function callers and function implementations and distinguishing between those expectations is a pure win.
If you are finding function coloring to be a big issue then the problem probably mainly lives in how you've organized your logic or codebase ... separating the colors so that things which should compose well do happens naturally in codebases which organize around data transformations (which most applications should aim toward)
Javascript handled async in probably the worst possible way. So now, effectively, everybody writes exclusively async functions, and calls them with await, regardless of whether it is necessary, because the introduction of an async call anywhere in the call stack will require every function in that stack to be changed to async in order to use the damn await keyword. And that change can have implications all over the codebase.
It looks like rust managed to avoid that tomfoolery by not introducing such a stupid rule.
But it's been a year since I tried so maybe executors are more likely to support arbitrarily-nested async -> sync -> async -> sync ... chains now.
Not sure if Rust does something similar, but C# is definitely coloured.
And so is Rust and IMHO any language (Go is mono red ;) ).
The more relevant take away is that not all drawbacks associated with coloring does apply to all languages equally.
IMHO the only constant about coloring is that calling functions of different color is somewhat harder then calling the same color (but how much depends on language).
This also means if your language default to blue/red then red/blue is likely to be generally slightly harder to use.
What “spawn it as a blocking task” mean? Is it ok to create a task using blocking non-async function?
So you can't at all carry over many of the arguments made there just like that.
Mainly in a more programming language agnostic way I would say:
1. We need at least three categories, instead of two: GRAY/UNCOLORED - function is fully call context independent; BLUE - function is expected to be called in a blocking/os-threaded context; RED - function is expected the be called in some form of async calling context.
1.1. The async context might be layered on top of a normal os context.
1.2. Function can potentially be compiler and or runtime generic over the calling context
1.3. There can be more calling contexts then RED and BLUE, e.g. interrupts and signal handlers.
2. You need to call the function in the right context, but if the way you call it changes at all is fully language specific. Furthermore type systems can prevent you from using it the "wrong way". Just because you have green thread support or similar doesn't mean you don't have colored functions. Especially when doing FFI function coloring often still is important to consider. Through languages might hide away one color as good as they can (which is always a best effort compromise, at least as long as you don't run a OS specifically made for the given prog. lang and even then hardware might still get in your way...)
3. You need to call functions in the right context, but that doesn't mean you can't change or layer the context inside of an function. E.g. the border between gray/uncolored and blue functions is vague so often everything is run in a blue context and a red context is in some places layered on top of a blue context. Through a language could decide to not do so! Still while you can run them in each other by ad-hook creating the right calling context, it is often not recommended due to performance aspects or increased likelihood of things like dead locks.
3.1. In rust we layer a red context on-top of a blue one, furthermore you can always create a red context inside of a blue function ad-hook. It's not perfect but super easy.
3.2. Lastly to some degree the whole red context in rust is just syntax/library sugar around using a blue context with certain conventions.
3.3. Any constraint about higher-order functions is not coloring but language specific. E.g. Rust currently supports functions as values (incl. closures). But it only has very limited support for higher kindred functions (or types) which are needed for having more than just basic higher-order functions. Which can currently impose some restrictions wrt. higher-ordered functions in an async context. The pending support for generic associated types should solve this problem.
4. Red functions need to be called in a red context, if this is more painful depends 100% on the language in question. Many languages default to a blue context, furthermore red context often allow MUCH more control over the flow of execution then blue contexts so it's not uncommon to have a bit different syntax around them. But if they are more painful to call is inherently a aspect of the programming language and not coloring on itself.
4.1. In rust you need to use the `.await` operator to call them. While you could argue that it's more painful, you can also argue it's less painful as it makes many thinks much less painful then a implicit await would.
4.1.1. A a side not I would argue in rust a `Future` is a red function and a `async` function or closure is red in source code but then get's compiled to a gray/uncolored function returning a red function. Which is more complex, but is also inherently what gives you more control over the execution of read functions.
5. Again super language dependent and not at all function coloring specific. Any language should either make the core libaris fully generic over the color, or provide both.
5.1. In rust all of core (and std) is gray/uncolored or blue. Read is in the end "just" layered on top and in core/std only supported as far as providing a (generator-like) code transformation and a view shared traits/utility types to allow impl. red context's as a library. If you consider such library a a core component of rust then rust has taken the path of providing a separate blue/red functions for all colored (non-gray) functionality.
So what is the takeaway? I would say the takeaway is that using arguments like "it's bad because of coloring" or well any argument using coloring as a reason why something is good or bad is fundamentally flawed. In the end it's not the coloring but the languages implementation of it which might or might not cause problems. This also means there are inherently more ways to fix a problem then there would be if coloring was actually at fault!