Hacker News new | past | comments | ask | show | jobs | submit login
Rust’s async isn’t colored (hobofan.com)
116 points by CyberRabbi 31 days ago | hide | past | favorite | 127 comments



Quoting this article's opinion on the 'What colour is your function' article

> 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.)


The original article was mainly centered around javascript, where you cannot spawn an executor to synchronously get the result of an async function.


> So the author starts off by declaring that the colored functions problem only applies to other languages but not to rust.

> 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.


You are getting pushback because people don't agree with your core assertion. The point of the colored function is that the color is a leaky abstraction. No matter the language you are using an async function call inside a sync function must be converted into a sync call and vice versa. Some languages make it easier than others to do but you can't escape the fact that you must do that conversion.

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.


> The point of the colored function is that the color is a leaky abstraction

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.


C# isn't exactly doing a great job at escaping the color world, judging by the number of bugs I've seen where calling .Wait() or .Result on an async function from a sync one caused the entire application to deadlock.


I'm not intimately familiar with C# so I take your word for it, but conceptual flaws and implementation bugs are two different things.


Usually the outcome of not calling ConfigureAwait().


I see your point, but I partially disagree.

> 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.


Just to double check I went back and re-read the article. It wasn't strictly about JS. It was about the entire concept of async await. In fact if you read it he dunks on C#, Dart (the language he works on), and python. He only calls out one language as having gotten it right.

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.


Agreed. Colored functions can be solved, though. See Unison[1] or Zig[2].

[1] https://jaredforsyth.com/posts/whats-cool-about-unison/

[2] https://kristoff.it/blog/zig-colorblind-async-await/


Zig has a really paradoxical reaction to “function coloring”: on the one hand, they create a specific system to avoid async/sync split, but on the other hand, because allocating functions need an allocator passed as argument, they are recreating the exact same problem in another place. (Allocating functions are red, zero-alloc are blue and you can't call a red function from a blue one).

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).


Rust has std and async_std as separate codebases because of function coloring. Zig has the same codebase that supports both evented I/O and blocking I/O because of the non-existence of function coloring.

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?


Hi Andrew, I'm a big fan of your work btw.

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.)


> Rust has std and async_std as separate codebases because of function coloring.

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.


This is silly.

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.


I tend to agree that allocator param isn't that strong of an argument (e.g. one can easily just store a pointer to the allocator in a struct to hide it from method signatures). And it is quite valid to just create a local allocator and `defer a.free(thing)` to clean up at the end of a function (for example, if you just need a trivial std.mem.join and you know that you can fit the result in stack). One does not need to use the top-level GPA/arena for everything.

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.

[0] https://github.com/ziglang/zig/issues/1006


> Allocating functions are red, zero-alloc are blue and you can't call a red function from a blue 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.


> There is no equivalent for async.

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.


No, your understanding is not correct. It's not "hackish" to embed your allocator in your object, sometimes it is the correct, domain driven choice to make. I know it's done somewhere in zig stdlib (parser, iirc).


This is a really good insight. There are some architecture decisions you really need to anticipate because if you don't, it will really be a pain in the ass to refactor.

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.


In a perfect word, this kind of refactor should be handled by the tooling (IDE/Language Server) though.


The painful aspect of async over the other "colors" is that they require the programmer to manually re-annotate all "recolored" functions.

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.


let f call g which calls h.

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.


I'm not usually a JS person, but you can do the same thing with Promises too, right?

Did JS Solve(tm) "colored" functions?


> but you can do the same thing with Promises too, right?

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.


You know what, you're right. I thought there was an "unwrap", that's my b


You can technically wrap the promise and spin lock on it till it resolves. Aka forcibly wait on an async function in a sync function. There are some npm libraries that provide it. I've used a couple times (for some hacky scripts), though obviously don't use it in the browser.


AFAIK, you can't do this from JS, you have to have some engine integration, as the busy loop in JS will prevent the event loop from working.

For instance, this will essentially halt your runtime:

    let y;
    Promise.resolve(123).then(x => { y = x; });

    while (!y) {}
    console.log(y)


Yes, deasync[0] for example, hooks into the Node.js event loop from c++ land

[0] https://www.npmjs.com/package/deasync


No problem, knowledge comes from suffering ;)


No, js won't allow you to halt the whole thread to wait for your async function (most runtime implements it as single thread). But kotlin on Java does (while you have absolutely no reason to do that except workaround, you just literally get a slower sync version of that function. It is a escape hole for you to do it sync when something absolutely need it be like that (take a example, the main function))


Indeed. Also, "very painful" is subjective. I never had issues with async code in JS (well, I had bugs, but it's not a fault of the language). There's nothing painful in async JS.


Agree, JavaScript's approach to concurrency and asynchronous events is one of its strong points. The model of There is exactly one thread, and everything that could be asynchronous is asynchronous works nicely for web work, keeping things relatively simple (of course, race conditions are still possible) while avoiding the pitfalls of full-bore multithreading. I imagine it also makes things far simpler for the browser engineers. It's the opposite of the old Java Swing approach, where applications would often have far more threads than they really needed. (iirc, NetBeans idles with something like 30 threads.)

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.


> There is exactly one thread, and everything that could be asynchronous is asynchronous works nicely for web work, keeping things relatively simple

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.


> Parallelism is where performance is going to come from in the future

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.


JS is not single threaded in the way you indicate.

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 JS really needs is a CSP or Actor model on top to abstract away threads/OS processes/cores/hardware

What kind of website would benefit from this?


SPA and PWAs.


Is the point here concurrency or parallelism?

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.


PWAs are the native applications of ChromeOS, WebOS, KaiOS, and by the looks of it, many mobile applications.


I'm not convinced it's the right move for the web to try to accommodate the development of every conceivable kind of GUI application. This doesn't change when someone makes a computer whose only GUI toolkit is a web browser.

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.


It will get ported to WebGL/GPU with WebAssembly, it is still Web.

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.


There are going to be webworkers which will allow you to use more than one thread. (Though it is technically a browser implementation so it'd be up for the browser to implement/allow it)


Genuine question, what would the ad trackers be able to do with runtime level parallelism in the JS runtime that they can’t achieve now? Do they do a lot more blocking calculation than I’d expect?


Stuff like parsing JSON is typically still blocking. I’m sure there are other forms of blocking computation.

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.


> Stuff like parsing JSON is typically still blocking. I’m sure there are other forms of blocking computation.

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.


Having to use an async immediately invoked function expression in Node scripts is kind of a pain, but I agree.


I haven't found it painful, but i avoid iife in favor of a named function I call...

``` async function main() { await stuff(); }

if (module === require.main) main(); ```


Top-level await is available in ES modules in Node without any flags since 14.8.0


> However this is not a problem in Rust! In Rust, you can easily confine the coloring to exactly the part of the code you want to, and doing it is pretty straightforward... Of course you shouldn't do both of those things all the time, or you will lose most of the benefits of using async in the first place!

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.


Javascript (where the color analogy is from) does not allow in any way calling promise/async/callback based functions as if they were blocking.

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.


If there are two types of functions with syntactically distinct calling mechanisms, and there is a cost, in both syntax and performance, for mixing them, then you have two colours. That the cost of mixing isn't infinite but just high -- high enough to require two distinct sets of APIs, one for each colour -- then you have two colours with all their issues, even if to a somewhat smaller extent than the extreme, and unusual, case of JS. If doing something is expensive and unrecommended, then doing it is very much an issue.


(Author here)

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.


The original article explicitly says it's not about syntax or performance. It's about irrecoverably splitting call graphs when sync functions can't wait for async results. JS has this issue, but Rust and C# don't.

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.


Most of the functions are syntactically distinct.


Javascript is particularly problematic as there is an uncrossable divide between the async and sync world. But any language with stackless coroutines has as similar problem where you cannot yield across a function stack unless the whole function stack cooperates (i.e. every function is async).


> > 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.

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.


> despite being the prime example of them

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.”


That article says

> 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.


That is such absurd hyperbole. Just because I can wrap an async function call in 10 lines of runtime-specific boilerplate doesn't make the function color problem any less annoying.


It stops unnecessary pollution when dealing with code you cannot control. Often you couldn't care less about the asynchronicity of some calls. Sometimes it's not even obvious why some things are async at all. You're forced to make your code async even when it was never meant to be async as per your mental model.


10 lines is fairly annoying. Fortunately, the color change bit alone is just 1 line - and you can mix runtimes, although the wisdom of doing so is questionable.

    println!("{}", futures::executor::block_on(library::read()).unwrap());
https://github.com/MaulingMonkey/untokio/blob/892fab053b4f6d...


Yes, that is exactly the point I was trying to make!

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.


Well, making a blocking call -say writing to a log for auditing purposes- in a tight loop can dramatically decrease performance.

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 will be no issue in terms of program correctness

there is if it leads to deadlocks.


"does not allow in any way calling promise/async/callback" To be accurate you can wrap your promise with a spin lock and wait on it if you really really want to. It's just a horrible idea for the browser. But yes you can wait on async functions if desired, the js language doesn't actually prevent it, there's just no built-in feature.


Does this actually work, though? I would expect it to fail similarly as this fails (note, may hang your browser or a tab, depending on your browser):

  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..


It doesn't, as promise resolution and execution is scheduled as new tasks on the main thread, which are only ever run if the currently executing piece of code finishes.

That's pretty much identical between IO, setTimeout/setInterval and requestAnimationFrame


The articles title F#king has nothing to do with the programming language F#, it is actually just masking a swear word.

I was confused in the beginning


Yeah, better to leave out the swear word completely from the HN entry, rather than write a confusing euphemism.


It was censored in the original with #@. I’m guessing HN doesn’t allow @ in post titles.


Well, it worked 7 months ago for this item: https://news.ycombinator.com/item?id=23895789


The space and capitalisation makes it even more confusing, `f#king` wouldn't have been so bad; `f##king` or `f*king` (or the more common vowel-omission style `f*cking`) better.


I think the resolution to the author's consternation is that Rust's/some/most async run-times have "good enough" (in his view) color management.

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.)


Calling a sync function from an async when using the tokio executor is literally documented as a thing you should not do. Furthermore if you want to then spawn a task or whatever it's called, which is the documented way to do this safely, then you have to capture with static lifetime. That is a hard guarantee to provide for sufficiently complex code! Rust is definitely colored, and it's not as simple as just spawning an executor


To spawn a sync function from an async context you use https://docs.rs/tokio/1.3.0/tokio/task/fn.spawn_blocking.htm....


I’m a big fan of Rust’s Future model, but it is still colored in the same way as most languages that offer a “block until future/promise/whatever is complete” (C#, Java, etc.). You can do that, but depending on your executor model it can vary from inefficient (e.g. blocking a thread on the thread pool that is supposed to be running futures) to deadlocking (e.g. a single-threaded executor, or blocking on all threads of a thread pool).

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.


Despite the title, the article accepts that function colours exist in Rust. The question is, does it matter?

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“?


In JavaScript, you can await a non-promise returning function and it's a NO-OP. So if you are taking a function as a parameter, you can await it safely, regardless of whether it's async or not.

   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);


Instead of

  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()))


The example uses `g(h2)`, not `g(h2())` - `g` takes the function `h2` as an argument, and the call(s) to `h2` are made from within `g` itself.

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.


exactly this. Unless the language allows abstracting over the async/non-async calls.


I'm not a Rust user, but this sounds similar to the situation in Scala, where you can call Await.result to block the thread on a Future to get its value. The problem is that now, any callers of the function that blocks are blocking. So one form of coloring, "async", is converted to another, "blocking". You can use multithreading and synchronization to break of this trap, but now you're in the world we built all of this to escape in the first place.

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.


Yeah, Scala doesn't have async/await but you have the same basic issue where if you have one async function (something that returns a Future/IO/Task/etc) then every caller has to also be async (or block on the result and potentially cause all manner of mayhem).

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).


Disagreed:

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 an F# king? The spouse of the C# queen?


Those who _champion_ F# features :p


Aren't all statically typed languages colored anyway? With something called a type.

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".


This speaks about a very different kind of coloring which applies to more or less any language (IMHO) but definetly not just statically typed languages.

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.


Most languages allow abstracting over types (for example via parametric polymorphism), but not over sync/async (although some might).


I ported a large Python codebase from sync to async. There's a temptation to partially move and keep a mixed async/sync structure. That temptation is wrong though.

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.


Rust has its own colored functions problems due to the whole Fn/FnMut/FnOnce distinction, based on the properties of closures' captured state. It's bad enough that some folks would say Rust functions are not genuinely first class, and it's also why some features that are common in other functional languages (including monads as generally understood in FP) are challenging in Rust.

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.


Doesn't the "colors" concept also apply to concepts like error handling? E.g. Result, Option, exceptions...

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.


It doesn't, because you can wrap and unwrap Results at will, so you can change "color" of such functions, and you're never irrecoverably stuck with just one color.

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.


That's what I'm saying, you can change the "color" of all these including async, but you still have to do that.

There is no single Monad type from which you can build and chain, you always have to color some calls.


No, you can abstract over "color".

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>`.


You are right, it doesn't "poison" your code, you can always wrap or map it.

I guess I was just going off some tangent.


Disclaimer: I am not a hard core consumer of async utilities and idioms.

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)


AOP is pretty much alive in each Spring application.


If we follow the logic of the article's author to the end, then JS doesn't have this problem either. You can await on non-Promise values, so can just say that everything is red, add await to every function call and call it a day.


I think of async functions in rust as regular functions that happen to return a Future. Sure to do something useful you need to use an executor (or follow the semantics of one) which uses a context to track the progress through a state machine until success or failure. But it isn't from a different "universe" (color) of functions. That's the big difference here from say Javascript. At least that's how I explain it to myself.


Colors seem to be about how functions compose, and the way functions compose is a way we can control effects.

"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.)


> However sometimes you don't really have a choice, and this super cool crate you want to use is async or sync (obviously always the version you don't need right now), and you have to make it work.

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


F# Async functions are colored, in case people are wondering.

However, you can "un-color" them like this:

    doAsyncThing
    |> Async.RunSynchronously


In rust `block_on(asyncFuncion())`.

Through 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())`.


> I personally don't mind it much, as that explicitness is one of my favourite things about Rust.

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.


> Rust has operator overloading, so the + operator may call a function, I don't call that explicit.

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.


What are some examples of non coloured async runtimes?


One of the more widely deployed ones is Go. Code looks like with synchronous IO, but everything runs in green threads that can be suspended when IO would block. So if you do a File.Read, after it returns, you might be running on a different OS thread because you were suspended mid-Read and later woken up on a different thread. It's generally nice to work with, unless you need to interface with a non-Go library that expects you to use a single thread throughout (e.g. for callbacks), in which case suddenly everything turns into a royal mess.


> everything runs in green threads that can be suspended when IO would block

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`.


Rust was originally built with green threads. The RFC that proposed removing them has an extremely detailed explanation of why the change was made pre-1.0: https://github.com/rust-lang/rfcs/blob/0806be4f282144cfcd55b...

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.


I don't think that's a very strong guarantee though.

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.


IMHO there are non!

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.


> 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.

well, that's exactly what is done in traditional C, C++, Java...


I am building an experimental front end language that does not have coloured functions:

https://hyperscript.org

The runtime works everything out at the expression level to handle promises so developers don't need to deal with them:

https://hyperscript.org/docs/#async


The original post was about Node.js, and doesn't mention Rust at all. Not sure why the author is so emotional.


Because miraculously once a comment section about async Rust is big enough, some people always start throwing around the coloring argument, and I'm getting tired of explaining why this doesn't apply to Rust, and thought I'd write something I can refer to in the future.

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


why do people think function coloring is bad? In my experience it's a very good thing ...

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)


Function coloring is bad because, as you say, the two function call types are massively different abstractions, while being almost interchangable. If two things are massively different, they shouldn't be interchangable.

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.


Last time I tried you couldn't call block_on inside another call to block_on, so you can't always just "throw it on the executor".

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.


If Rust async isn't colored, so C#'s.


In C# one has to take the synchronization context into account. Calling an async method in a synchronous method while blocking, will lead to deadlocks sooner or later on various implementations. For example ASP.NET (non-Core) only allows 1 thread to enter the context. Now you could sprinkle some ConfigureAwait(false) over your code, but that means you can no longer access HttpContext inside your code.

Not sure if Rust does something similar, but C# is definitely coloured.


> 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.


the article say “ Want to call a sync function from an async function? -> Either just call in normally, or if it would block your async function spawn it as a blocking task”

What “spawn it as a blocking task” mean? Is it ok to create a task using blocking non-async function?


I would argue rust is colored (and I'm not sure if there are any languages which are truly uncolored) but I would argue the original article this links to is quite specific to JS and more specific pre-async/await(promise) JS!!

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!


Rust's async just doesn't worth the hassle. At the moment when you have enough requests to get the benefits from async over thread::spawn, you'll HAVE TO run multiple instances/servers, at least for fail-safety reasons. And when the load is balanced, all the async benefits are not enough to be noticed, so there are no reasons to step into this mess.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: